diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index c0938fdadc..12f913b9e7 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -307,7 +307,7 @@ - + @@ -417,7 +417,7 @@ - + @@ -480,7 +480,7 @@ - + @@ -564,7 +564,7 @@ - + @@ -775,6 +775,8 @@ + + diff --git a/OpenRA.Mods.Common/Traits/Carryable.cs b/OpenRA.Mods.Common/Traits/Carryable.cs index 8f45f29a2b..c2a1c3e8b9 100644 --- a/OpenRA.Mods.Common/Traits/Carryable.cs +++ b/OpenRA.Mods.Common/Traits/Carryable.cs @@ -52,6 +52,8 @@ namespace OpenRA.Mods.Common.Traits protected override void Created(Actor self) { upgradeManager = self.Trait(); + + base.Created(self); } public virtual void Attached(Actor self) diff --git a/OpenRA.Mods.Common/Traits/Crates/GrantUpgradeCrateAction.cs b/OpenRA.Mods.Common/Traits/Crates/GrantExternalConditionCrateAction.cs similarity index 65% rename from OpenRA.Mods.Common/Traits/Crates/GrantUpgradeCrateAction.cs rename to OpenRA.Mods.Common/Traits/Crates/GrantExternalConditionCrateAction.cs index 3482ad574e..daa82819b0 100644 --- a/OpenRA.Mods.Common/Traits/Crates/GrantUpgradeCrateAction.cs +++ b/OpenRA.Mods.Common/Traits/Crates/GrantExternalConditionCrateAction.cs @@ -15,11 +15,11 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("Grants an upgrade to the collector.")] - public class GrantUpgradeCrateActionInfo : CrateActionInfo + public class GrantExternalConditionCrateActionInfo : CrateActionInfo { - [UpgradeGrantedReference, FieldLoader.Require] - [Desc("The upgrades to apply.")] - public readonly string[] Upgrades = { }; + [FieldLoader.Require] + [Desc("The condition to apply. Must be included in the target actor's ExternalConditions list.")] + public readonly string Condition = null; [Desc("Duration of the upgrade (in ticks). Set to 0 for a permanent upgrade.")] public readonly int Duration = 0; @@ -30,37 +30,36 @@ namespace OpenRA.Mods.Common.Traits [Desc("The maximum number of extra collectors to grant the crate action to.", "-1 = no limit")] public readonly int MaxExtraCollectors = 4; - public override object Create(ActorInitializer init) { return new GrantUpgradeCrateAction(init.Self, this); } + public override object Create(ActorInitializer init) { return new GrantExternalConditionCrateAction(init.Self, this); } } - public class GrantUpgradeCrateAction : CrateAction + public class GrantExternalConditionCrateAction : CrateAction { readonly Actor self; - readonly GrantUpgradeCrateActionInfo info; + readonly GrantExternalConditionCrateActionInfo info; - public GrantUpgradeCrateAction(Actor self, GrantUpgradeCrateActionInfo info) + public GrantExternalConditionCrateAction(Actor self, GrantExternalConditionCrateActionInfo info) : base(self, info) { this.self = self; this.info = info; } - bool AcceptsUpgrade(Actor a) + bool AcceptsCondition(Actor a) { var um = a.TraitOrDefault(); - return um != null && (info.Duration > 0 ? - info.Upgrades.Any(u => um.AcknowledgesUpgrade(a, u)) : info.Upgrades.Any(u => um.AcceptsUpgrade(a, u))); + return um != null && um.AcceptsExternalCondition(a, info.Condition); } public override int GetSelectionShares(Actor collector) { - return AcceptsUpgrade(collector) ? info.SelectionShares : 0; + return AcceptsCondition(collector) ? info.SelectionShares : 0; } public override void Activate(Actor collector) { var actorsInRange = self.World.FindActorsInCircle(self.CenterPosition, info.Range) - .Where(a => a != self && a != collector && a.Owner == collector.Owner && AcceptsUpgrade(a)); + .Where(a => a != self && a != collector && a.Owner == collector.Owner && AcceptsCondition(a)); if (info.MaxExtraCollectors > -1) actorsInRange = actorsInRange.Take(info.MaxExtraCollectors); @@ -73,19 +72,10 @@ namespace OpenRA.Mods.Common.Traits continue; var um = a.TraitOrDefault(); - foreach (var u in info.Upgrades) - { - if (info.Duration > 0) - { - if (um.AcknowledgesUpgrade(a, u)) - um.GrantTimedUpgrade(a, u, info.Duration); - } - else - { - if (um.AcceptsUpgrade(a, u)) - um.GrantUpgrade(a, u, this); - } - } + + // Condition token is ignored because we never revoke this condition. + if (um != null) + um.GrantCondition(a, info.Condition, true, info.Duration); } }); diff --git a/OpenRA.Mods.Common/Traits/Render/TimedUpgradeBar.cs b/OpenRA.Mods.Common/Traits/Render/TimedConditionBar.cs similarity index 65% rename from OpenRA.Mods.Common/Traits/Render/TimedUpgradeBar.cs rename to OpenRA.Mods.Common/Traits/Render/TimedConditionBar.cs index 7298e40f8f..c97d5f77cd 100644 --- a/OpenRA.Mods.Common/Traits/Render/TimedUpgradeBar.cs +++ b/OpenRA.Mods.Common/Traits/Render/TimedConditionBar.cs @@ -15,38 +15,35 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits.Render { [Desc("Visualizes the remaining time for an upgrade.")] - class TimedUpgradeBarInfo : ITraitInfo, Requires + class TimedConditionBarInfo : ITraitInfo, Requires { [FieldLoader.Require] - [Desc("Upgrade that this bar corresponds to")] - public readonly string Upgrade = null; + [Desc("Condition that this bar corresponds to")] + public readonly string Condition = null; public readonly Color Color = Color.Red; - public object Create(ActorInitializer init) { return new TimedUpgradeBar(init.Self, this); } + public object Create(ActorInitializer init) { return new TimedConditionBar(init.Self, this); } } - class TimedUpgradeBar : ISelectionBar, INotifyCreated + class TimedConditionBar : ISelectionBar, IConditionTimerWatcher { - readonly TimedUpgradeBarInfo info; + readonly TimedConditionBarInfo info; readonly Actor self; float value; - public TimedUpgradeBar(Actor self, TimedUpgradeBarInfo info) + public TimedConditionBar(Actor self, TimedConditionBarInfo info) { this.self = self; this.info = info; } - public void Created(Actor self) + void IConditionTimerWatcher.Update(int duration, int remaining) { - self.Trait().RegisterWatcher(info.Upgrade, Update); + value = duration > 0 ? remaining * 1f / duration : 0; } - public void Update(int duration, int remaining) - { - value = remaining * 1f / duration; - } + string IConditionTimerWatcher.Condition { get { return info.Condition; } } float ISelectionBar.GetValue() { diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/GrantUpgradePower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/GrantExternalConditionPower.cs similarity index 75% rename from OpenRA.Mods.Common/Traits/SupportPowers/GrantUpgradePower.cs rename to OpenRA.Mods.Common/Traits/SupportPowers/GrantExternalConditionPower.cs index 1a5aba77fe..74575a64dd 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/GrantUpgradePower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/GrantExternalConditionPower.cs @@ -19,31 +19,33 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { - class GrantUpgradePowerInfo : SupportPowerInfo + class GrantExternalConditionPowerInfo : SupportPowerInfo { - [UpgradeGrantedReference, FieldLoader.Require] - [Desc("The upgrades to apply.")] - public readonly string[] Upgrades = { }; + [FieldLoader.Require] + [Desc("The condition to apply. Must be included in the target actor's ExternalConditions list.")] + public readonly string Condition = null; - [Desc("Duration of the upgrade (in ticks). Set to 0 for a permanent upgrade.")] + [Desc("Duration of the upgrade (in ticks). Set to 0 for a permanent condition.")] public readonly int Duration = 0; [Desc("Cells - affects whole cells only")] public readonly int Range = 1; - public readonly string GrantUpgradeSound = "ironcur9.aud"; + + [Desc("Sound to instantly play at the targeted area.")] + public readonly string OnFireSound = null; [SequenceReference, Desc("Sequence to play for granting actor when activated.", "This requires the actor to have the WithSpriteBody trait or one of its derivatives.")] - public readonly string GrantUpgradeSequence = "active"; + public readonly string Sequence = "active"; - public override object Create(ActorInitializer init) { return new GrantUpgradePower(init.Self, this); } + public override object Create(ActorInitializer init) { return new GrantExternalConditionPower(init.Self, this); } } - class GrantUpgradePower : SupportPower + class GrantExternalConditionPower : SupportPower { - GrantUpgradePowerInfo info; + readonly GrantExternalConditionPowerInfo info; - public GrantUpgradePower(Actor self, GrantUpgradePowerInfo info) + public GrantExternalConditionPower(Actor self, GrantExternalConditionPowerInfo info) : base(self, info) { this.info = info; @@ -60,30 +62,18 @@ namespace OpenRA.Mods.Common.Traits base.Activate(self, order, manager); var wsb = self.TraitOrDefault(); - if (wsb != null && wsb.DefaultAnimation.HasSequence(info.GrantUpgradeSequence)) - wsb.PlayCustomAnimation(self, info.GrantUpgradeSequence, () => wsb.CancelCustomAnimation(self)); + if (wsb != null && wsb.DefaultAnimation.HasSequence(info.Sequence)) + wsb.PlayCustomAnimation(self, info.Sequence, () => wsb.CancelCustomAnimation(self)); - Game.Sound.Play(info.GrantUpgradeSound, self.World.Map.CenterOfCell(order.TargetLocation)); + Game.Sound.Play(info.OnFireSound, self.World.Map.CenterOfCell(order.TargetLocation)); foreach (var a in UnitsInRange(order.TargetLocation)) { var um = a.TraitOrDefault(); - if (um == null) - continue; - foreach (var u in info.Upgrades) - { - if (info.Duration > 0) - { - if (um.AcknowledgesUpgrade(a, u)) - um.GrantTimedUpgrade(a, u, info.Duration); - } - else - { - if (um.AcceptsUpgrade(a, u)) - um.GrantUpgrade(a, u, this); - } - } + // Condition token is ignored because we never revoke this condition. + if (um != null) + um.GrantCondition(a, info.Condition, true, info.Duration); } } @@ -101,20 +91,19 @@ namespace OpenRA.Mods.Common.Traits return false; var um = a.TraitOrDefault(); - return um != null && (info.Duration > 0 ? - info.Upgrades.Any(u => um.AcknowledgesUpgrade(a, u)) : info.Upgrades.Any(u => um.AcceptsUpgrade(a, u))); + return um != null && um.AcceptsExternalCondition(a, info.Condition); }); } class SelectUpgradeTarget : IOrderGenerator { - readonly GrantUpgradePower power; + readonly GrantExternalConditionPower power; readonly int range; readonly Sprite tile; readonly SupportPowerManager manager; readonly string order; - public SelectUpgradeTarget(World world, string order, SupportPowerManager manager, GrantUpgradePower power) + public SelectUpgradeTarget(World world, string order, SupportPowerManager manager, GrantExternalConditionPower power) { // Clear selection if using Left-Click Orders if (Game.Settings.Game.UseClassicMouseStyle) diff --git a/OpenRA.Mods.Common/Traits/Upgrades/ExternalConditions.cs b/OpenRA.Mods.Common/Traits/Upgrades/ExternalConditions.cs new file mode 100644 index 0000000000..c544a6c188 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Upgrades/ExternalConditions.cs @@ -0,0 +1,25 @@ +#region Copyright & License Information +/* + * Copyright 2007-2016 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 OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Lists conditions that are accepted from external sources (Lua, warheads, etc).", + "Externally granted conditions that aren't explicitly whitelisted will be silently ignored.")] + public class ExternalConditionsInfo : TraitInfo + { + [UpgradeGrantedReference] + public readonly string[] Conditions = { }; + } + + public class ExternalConditions { } +} diff --git a/OpenRA.Mods.Common/Traits/Upgrades/StackedCondition.cs b/OpenRA.Mods.Common/Traits/Upgrades/StackedCondition.cs new file mode 100644 index 0000000000..3d0e6d2c69 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Upgrades/StackedCondition.cs @@ -0,0 +1,32 @@ +#region Copyright & License Information +/* + * Copyright 2007-2016 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 OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Grant additional conditions when a specified condition has been granted multiple times.")] + public class StackedConditionInfo : TraitInfo + { + [FieldLoader.Require] + [UpgradeUsedReference] + [Desc("Condition to monitor.")] + public readonly string Condition = null; + + [FieldLoader.Require] + [UpgradeGrantedReference] + [Desc("Conditions to grant when the monitored condition is granted multiple times.", + "The first entry is activated at 2x grants, second entry at 3x grants, and so on.")] + public readonly string[] StackedConditions = { }; + } + + public class StackedCondition { } +} diff --git a/OpenRA.Mods.Common/Traits/Upgrades/UpgradeManager.cs b/OpenRA.Mods.Common/Traits/Upgrades/UpgradeManager.cs index d7a07ef96e..4c49bbbfc8 100644 --- a/OpenRA.Mods.Common/Traits/Upgrades/UpgradeManager.cs +++ b/OpenRA.Mods.Common/Traits/Upgrades/UpgradeManager.cs @@ -17,6 +17,13 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { + [RequireExplicitImplementation] + public interface IConditionTimerWatcher + { + string Condition { get; } + void Update(int duration, int remaining); + } + [Desc("Attach this to a unit to enable dynamic upgrades by warheads, experience, crates, support powers, etc.")] public class UpgradeManagerInfo : TraitInfo, Requires { } @@ -25,38 +32,16 @@ namespace OpenRA.Mods.Common.Traits /// Value used to represent an invalid token. public static readonly int InvalidConditionToken = -1; - class TimedCondition + class ConditionTimer { - public class ConditionSource - { - public readonly object Source; - public int Remaining; - - public ConditionSource(int duration, object source) - { - Remaining = duration; - Source = source; - } - } - - public readonly string Condition; + public readonly int Token; public readonly int Duration; - public readonly HashSet Sources; - public int Remaining; // Equal to maximum of all Sources.Remaining + public int Remaining; - public TimedCondition(string condition, int duration, object source) + public ConditionTimer(int token, int duration) { - Condition = condition; - Duration = duration; - Remaining = duration; - Sources = new HashSet { new ConditionSource(duration, source) }; - } - - public void Tick() - { - Remaining--; - foreach (var source in Sources) - source.Remaining--; + Token = token; + Duration = Remaining = duration; } } @@ -69,23 +54,31 @@ namespace OpenRA.Mods.Common.Traits public readonly HashSet Tokens = new HashSet(); /// External callbacks that are to be executed when a timed condition changes. - public readonly List> Watchers = new List>(); + public readonly List Watchers = new List(); } - readonly List timedConditions = new List(); - Dictionary state; + readonly Dictionary> timers = new Dictionary>(); /// Each granted condition receives a unique token that is used when revoking. Dictionary tokens = new Dictionary(); + /// Set of whitelisted externally grantable conditions cached from ExternalConditions traits. + string[] externalConditions = { }; + + /// Set of conditions that are monitored for stacked bonuses, and the bonus conditions that they grant. + readonly Dictionary stackedConditions = new Dictionary(); + + /// Tokens granted by the stacked condition bonuses defined in stackedConditions. + readonly Dictionary> stackedTokens = new Dictionary>(); + int nextToken = 1; /// Temporary shim between the old and new upgrade/condition grant and revoke methods. - Dictionary, int> objectTokenShim = new Dictionary, int>(); + readonly Dictionary, int> objectTokenShim = new Dictionary, int>(); /// Cache of condition -> enabled state for quick evaluation of boolean conditions. - Dictionary conditionCache = new Dictionary(); + readonly Dictionary conditionCache = new Dictionary(); /// Read-only version of conditionCache that is passed to IConditionConsumers. IReadOnlyDictionary readOnlyConditionCache; @@ -96,12 +89,19 @@ namespace OpenRA.Mods.Common.Traits readOnlyConditionCache = new ReadOnlyDictionary(conditionCache); var allConsumers = new HashSet(); + var allWatchers = self.TraitsImplementing().ToList(); + foreach (var consumer in self.TraitsImplementing()) { allConsumers.Add(consumer); foreach (var condition in consumer.Conditions) { - state.GetOrAdd(condition).Consumers.Add(consumer); + var cs = state.GetOrAdd(condition); + cs.Consumers.Add(consumer); + foreach (var w in allWatchers) + if (w.Condition == condition) + cs.Watchers.Add(w); + conditionCache[condition] = false; } } @@ -117,6 +117,18 @@ namespace OpenRA.Mods.Common.Traits conditionCache[kv.Value] = conditionState.Tokens.Count > 0; } + // Build external condition whitelist + externalConditions = self.Info.TraitInfos() + .SelectMany(t => t.Conditions) + .Distinct() + .ToArray(); + + foreach (var sc in self.Info.TraitInfos()) + { + stackedConditions[sc.Condition] = sc.StackedConditions; + stackedTokens[sc.Condition] = new Stack(); + } + // Update all traits with their initial condition state foreach (var consumer in allConsumers) consumer.ConditionsChanged(self, readOnlyConditionCache); @@ -137,15 +149,35 @@ namespace OpenRA.Mods.Common.Traits foreach (var t in conditionState.Consumers) t.ConditionsChanged(self, readOnlyConditionCache); + + string[] sc; + if (stackedConditions.TryGetValue(condition, out sc)) + { + var target = (conditionState.Tokens.Count - 1).Clamp(0, sc.Length); + var st = stackedTokens[condition]; + for (var i = st.Count; i < target; i++) + st.Push(GrantCondition(self, sc[i])); + + for (var i = st.Count; i > target; i--) + RevokeCondition(self, st.Pop()); + } } /// Grants a specified condition. /// The token that is used to revoke this condition. - public int GrantCondition(Actor self, string condition) + /// Validate against the external condition whitelist. + /// Automatically revoke condition after this delay if non-zero. + public int GrantCondition(Actor self, string condition, bool external = false, int duration = 0) { + if (external && !externalConditions.Contains(condition)) + return InvalidConditionToken; + var token = nextToken++; tokens.Add(token, condition); + if (duration > 0) + timers.GetOrAdd(condition).Add(new ConditionTimer(token, duration)); + // Conditions may be granted before the state is initialized. // These conditions will be processed in INotifyCreated.Created. if (state != null) @@ -155,7 +187,7 @@ namespace OpenRA.Mods.Common.Traits } /// Revokes a previously granted condition. - /// The invalid token ID + /// The invalid token ID. /// The token ID returned by GrantCondition. public int RevokeCondition(Actor self, int token) { @@ -165,6 +197,15 @@ namespace OpenRA.Mods.Common.Traits tokens.Remove(token); + // Clean up timers + List ct; + if (timers.TryGetValue(condition, out ct)) + { + ct.RemoveAll(t => t.Token == token); + if (!ct.Any()) + timers.Remove(condition); + } + // Conditions may be granted and revoked before the state is initialized. if (state != null) UpdateConditionState(self, condition, token, true); @@ -172,6 +213,61 @@ namespace OpenRA.Mods.Common.Traits return InvalidConditionToken; } + /// Returns true if the given external condition will have an effect on this actor. + public bool AcceptsExternalCondition(Actor self, string condition) + { + if (state == null) + throw new InvalidOperationException("AcceptsExternalCondition cannot be queried before the actor has been fully created."); + + if (!externalConditions.Contains(condition)) + return false; + + string[] sc; + if (stackedConditions.TryGetValue(condition, out sc)) + return stackedTokens[condition].Count < sc.Length; + + return !conditionCache[condition]; + } + + /// Returns whether the specified token is valid for RevokeCondition + public bool TokenValid(Actor self, int token) + { + return tokens.ContainsKey(token); + } + + readonly HashSet timersToRemove = new HashSet(); + void ITick.Tick(Actor self) + { + // Watchers will be receiving notifications while the condition is enabled. + // They will also be provided with the number of ticks before the condition is disabled, + // as well as the duration of the longest active instance. + foreach (var kv in timers) + { + var duration = 0; + var remaining = 0; + foreach (var t in kv.Value) + { + if (--t.Remaining <= 0) + timersToRemove.Add(t.Token); + + // Track the duration and remaining time for the longest remaining timer + if (t.Remaining > remaining) + { + duration = t.Duration; + remaining = t.Remaining; + } + } + + foreach (var w in state[kv.Key].Watchers) + w.Update(duration, remaining); + } + + foreach (var t in timersToRemove) + RevokeCondition(self, t); + + timersToRemove.Clear(); + } + #region Shim methods for legacy upgrade granting code void CheckCanManageConditions() @@ -180,36 +276,12 @@ namespace OpenRA.Mods.Common.Traits throw new InvalidOperationException("Conditions cannot be managed until the actor has been fully created."); } - /// Upgrade level increments are limited to dupesAllowed per source, i.e., if a single - /// source attempts granting more upgrades than dupesAllowed, they will not accumulate. They will - /// replace each other instead, leaving only the most recently granted upgrade active. Each new - /// upgrade granting request will increment the upgrade's level until AcceptsUpgrade starts - /// returning false. Then, when no new levels are accepted, the upgrade source with the shortest - /// remaining upgrade duration will be replaced by the new source. public void GrantTimedUpgrade(Actor self, string upgrade, int duration, object source = null, int dupesAllowed = 1) { - var timed = timedConditions.FirstOrDefault(u => u.Condition == upgrade); - if (timed == null) - { - timed = new TimedCondition(upgrade, duration, source); - timedConditions.Add(timed); - GrantUpgrade(self, upgrade, timed); - return; - } - - var srcs = timed.Sources.Where(s => s.Source == source); - if (srcs.Count() < dupesAllowed) - { - timed.Sources.Add(new TimedCondition.ConditionSource(duration, source)); - if (AcceptsUpgrade(self, upgrade)) - GrantUpgrade(self, upgrade, timed); - else - timed.Sources.Remove(timed.Sources.MinBy(s => s.Remaining)); - } - else - srcs.MinBy(s => s.Remaining).Remaining = duration; - - timed.Remaining = Math.Max(duration, timed.Remaining); + CheckCanManageConditions(); + var token = GrantCondition(self, upgrade, false, duration); + if (source != null) + objectTokenShim[Pair.New(source, upgrade)] = token; } public void GrantUpgrade(Actor self, string upgrade, object source) @@ -242,39 +314,6 @@ namespace OpenRA.Mods.Common.Traits return !enabled; } - public void RegisterWatcher(string upgrade, Action action) - { - CheckCanManageConditions(); - - ConditionState s; - if (!state.TryGetValue(upgrade, out s)) - return; - - s.Watchers.Add(action); - } - - /// Watchers will be receiving notifications while the condition is enabled. - /// They will also be provided with the number of ticks before the condition is disabled, - /// as well as the duration in ticks of the timed upgrade (provided in the first call to - /// GrantTimedUpgrade). - void ITick.Tick(Actor self) - { - foreach (var u in timedConditions) - { - u.Tick(); - foreach (var source in u.Sources) - if (source.Remaining <= 0) - RevokeUpgrade(self, u.Condition, u); - - u.Sources.RemoveWhere(source => source.Remaining <= 0); - - foreach (var a in state[u.Condition].Watchers) - a(u.Duration, u.Remaining); - } - - timedConditions.RemoveAll(u => u.Remaining <= 0); - } - #endregion } } diff --git a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs index 7718eca762..d0bac4db7c 100644 --- a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs +++ b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs @@ -92,6 +92,28 @@ namespace OpenRA.Mods.Common.UtilityCommands catch { } } + static void RenameNodeKey(MiniYamlNode node, string key) + { + var parts = node.Key.Split('@'); + node.Key = key; + if (parts.Length > 1) + node.Key += "@" + parts[1]; + } + + static void ConvertUpgradesToCondition(MiniYamlNode parent, MiniYamlNode node, string upgradesKey, string conditionKey) + { + var upgradesNode = node.Value.Nodes.FirstOrDefault(n => n.Key == upgradesKey); + if (upgradesNode != null) + { + var conditions = FieldLoader.GetValue("", upgradesNode.Value.Value); + if (conditions.Length > 1) + Console.WriteLine("Unable to automatically migrate {0}:{1} {2} to {3}. This must be corrected manually", + parent.Key, node.Key, upgradesKey, conditionKey); + else + upgradesNode.Key = conditionKey; + } + } + internal static void UpgradeActorRules(ModData modData, int engineVersion, ref List nodes, MiniYamlNode parent, int depth) { var addNodes = new List(); @@ -118,10 +140,7 @@ namespace OpenRA.Mods.Common.UtilityCommands if (s != null) s.Key = "Image"; - var parts = node.Key.Split('@'); - node.Key = "WithDamageOverlay"; - if (parts.Length > 1) - node.Key += "@" + parts[1]; + RenameNodeKey(node, "WithDamageOverlay"); } } @@ -135,13 +154,9 @@ namespace OpenRA.Mods.Common.UtilityCommands if (engineVersion < 20160611) { // Deprecated WithSpriteRotorOverlay - if (depth == 1 && node.Key.StartsWith("WithSpriteRotorOverlay")) + if (depth == 1 && node.Key.StartsWith("WithSpriteRotorOverlay", StringComparison.Ordinal)) { - var parts = node.Key.Split('@'); - node.Key = "WithIdleOverlay"; - if (parts.Length > 1) - node.Key += "@" + parts[1]; - + RenameNodeKey(node, "WithIdleOverlay"); Console.WriteLine("The 'WithSpriteRotorOverlay' trait has been removed."); Console.WriteLine("Its functionality can be fully replicated with 'WithIdleOverlay' + upgrades."); Console.WriteLine("Look at the helicopters in our RA / C&C1 mods for implementation details."); @@ -282,13 +297,8 @@ namespace OpenRA.Mods.Common.UtilityCommands if (engineVersion < 20160818) { - if (depth == 1 && node.Key.StartsWith("UpgradeOnDamage")) - { - var parts = node.Key.Split('@'); - node.Key = "UpgradeOnDamageState"; - if (parts.Length > 1) - node.Key += "@" + parts[1]; - } + if (depth == 1 && node.Key.StartsWith("UpgradeOnDamage", StringComparison.Ordinal)) + RenameNodeKey(node, "UpgradeOnDamageState"); } // DisplayTimer was replaced by DisplayTimerStances @@ -435,7 +445,7 @@ namespace OpenRA.Mods.Common.UtilityCommands } } - // Rename Replaced upgrade consumers with conditions + // Replaced upgrade consumers with conditions if (engineVersion < 20161117) { var upgradeTypesNode = node.Value.Nodes.FirstOrDefault(n => n.Key == "UpgradeTypes"); @@ -480,16 +490,7 @@ namespace OpenRA.Mods.Common.UtilityCommands if (engineVersion < 20161119) { // Migrated carryalls over to new conditions system - var carryableUpgradesNode = node.Value.Nodes.FirstOrDefault(n => n.Key == "CarryableUpgrades"); - if (carryableUpgradesNode != null) - { - var conditions = FieldLoader.GetValue("", carryableUpgradesNode.Value.Value); - if (conditions.Length > 1) - Console.WriteLine("Unable to automatically migrate {0}:{1} CarryableUpgrades to CarriedCondition. This must be corrected manually", - parent.Key, node.Key); - else - carryableUpgradesNode.Key = "CarriedCondition"; - } + ConvertUpgradesToCondition(parent, node, "CarryableUpgrades", "CarriedCondition"); if (node.Key == "WithDecorationCarryable") { @@ -498,6 +499,39 @@ namespace OpenRA.Mods.Common.UtilityCommands } } + if (engineVersion < 20161120) + { + if (node.Key.StartsWith("TimedUpgradeBar", StringComparison.Ordinal)) + { + RenameNodeKey(node, "TimedConditionBar"); + ConvertUpgradesToCondition(parent, node, "Upgrade", "Condition"); + } + + if (node.Key.StartsWith("GrantUpgradePower", StringComparison.Ordinal)) + { + Console.WriteLine("GrantUpgradePower Condition must be manually added to all target actor's ExternalConditions list."); + RenameNodeKey(node, "GrantExternalConditionPower"); + ConvertUpgradesToCondition(parent, node, "Upgrades", "Condition"); + + var soundNode = node.Value.Nodes.FirstOrDefault(n => n.Key == "GrantUpgradeSound"); + if (soundNode != null) + soundNode.Key = "OnFireSound"; + else + node.Value.Nodes.Add(new MiniYamlNode("OnFireSound", "ironcur9.aud")); + + var sequenceNode = node.Value.Nodes.FirstOrDefault(n => n.Key == "GrantUpgradeSequence"); + if (sequenceNode != null) + sequenceNode.Key = "Sequence"; + } + + if (node.Key.StartsWith("GrantUpgradeCrateAction", StringComparison.Ordinal)) + { + Console.WriteLine("GrantUpgradeCrateAction Condition must be manually added to all target actor's ExternalConditions list."); + RenameNodeKey(node, "GrantExternalConditionCrateAction"); + ConvertUpgradesToCondition(parent, node, "Upgrades", "Condition"); + } + } + UpgradeActorRules(modData, engineVersion, ref node.Value.Nodes, node, depth + 1); } @@ -567,6 +601,16 @@ namespace OpenRA.Mods.Common.UtilityCommands node.Key = "LaunchAngle"; } + if (engineVersion < 20161120) + { + if (node.Key.StartsWith("Warhead", StringComparison.Ordinal) && node.Value.Value == "GrantUpgrade") + { + node.Value.Value = "GrantExternalCondition"; + Console.WriteLine("GrantExternalCondition Condition must be manually added to all target actor's ExternalConditions list."); + ConvertUpgradesToCondition(parent, node, "Upgrades", "Condition"); + } + } + UpgradeWeaponRules(modData, engineVersion, ref node.Value.Nodes, node, depth + 1); } } diff --git a/OpenRA.Mods.Common/Warheads/GrantUpgradeWarhead.cs b/OpenRA.Mods.Common/Warheads/GrantExternalConditionWarhead.cs similarity index 63% rename from OpenRA.Mods.Common/Warheads/GrantUpgradeWarhead.cs rename to OpenRA.Mods.Common/Warheads/GrantExternalConditionWarhead.cs index add510f084..73173a4f6e 100644 --- a/OpenRA.Mods.Common/Warheads/GrantUpgradeWarhead.cs +++ b/OpenRA.Mods.Common/Warheads/GrantExternalConditionWarhead.cs @@ -10,19 +10,18 @@ #endregion using System.Collections.Generic; -using System.Linq; using OpenRA.Mods.Common.Traits; using OpenRA.Traits; namespace OpenRA.Mods.Common.Warheads { - public class GrantUpgradeWarhead : Warhead + public class GrantExternalConditionWarhead : Warhead { - [UpgradeGrantedReference] - [Desc("The upgrades to apply.")] - public readonly string[] Upgrades = { }; + [FieldLoader.Require] + [Desc("The condition to apply. Must be included in the target actor's ExternalConditions list.")] + public readonly string Condition = null; - [Desc("Duration of the upgrade (in ticks). Set to 0 for a permanent upgrade.")] + [Desc("Duration of the condition (in ticks). Set to 0 for a permanent condition.")] public readonly int Duration = 0; public readonly WDist Range = WDist.FromCells(1); @@ -38,22 +37,10 @@ namespace OpenRA.Mods.Common.Warheads continue; var um = a.TraitOrDefault(); - if (um == null) - continue; - foreach (var u in Upgrades) - { - if (Duration > 0) - { - if (um.AcknowledgesUpgrade(a, u)) - um.GrantTimedUpgrade(a, u, Duration, firedBy, Upgrades.Count(upg => upg == u)); - } - else - { - if (um.AcceptsUpgrade(a, u)) - um.GrantUpgrade(a, u, this); - } - } + // Condition token is ignored because we never revoke this condition. + if (um != null && um.AcceptsExternalCondition(a, Condition)) + um.GrantCondition(a, Condition, true, Duration); } } } diff --git a/mods/cnc/rules/defaults.yaml b/mods/cnc/rules/defaults.yaml index 6786586e8c..79efc71355 100644 --- a/mods/cnc/rules/defaults.yaml +++ b/mods/cnc/rules/defaults.yaml @@ -159,6 +159,8 @@ CloakSound: trans1.aud UncloakSound: trans1.aud RequiresCondition: cloak + ExternalConditions@CLOAK: + Conditions: cloak MustBeDestroyed: Voiced: VoiceSet: VehicleVoice diff --git a/mods/cnc/rules/misc.yaml b/mods/cnc/rules/misc.yaml index 52a1e267bd..efeab016f0 100644 --- a/mods/cnc/rules/misc.yaml +++ b/mods/cnc/rules/misc.yaml @@ -16,10 +16,10 @@ CRATE: ExplodeCrateAction@fire: Weapon: Napalm.Crate SelectionShares: 5 - GrantUpgradeCrateAction@cloak: + GrantExternalConditionCrateAction@cloak: SelectionShares: 5 Effect: cloak - Upgrades: cloak + Condition: cloak GiveMcvCrateAction: SelectionShares: 0 NoBaseSelectionShares: 120 diff --git a/mods/ra/maps/bomber-john/rules.yaml b/mods/ra/maps/bomber-john/rules.yaml index dd1f3b6566..2d92b36338 100644 --- a/mods/ra/maps/bomber-john/rules.yaml +++ b/mods/ra/maps/bomber-john/rules.yaml @@ -119,7 +119,7 @@ FTUR: BeginChargeSound: chrochr1.aud EndChargeSound: chrordy1.aud Range: 3 - GrantUpgradePower@IRONCURTAIN: + GrantExternalConditionPower@IRONCURTAIN: Icon: invuln ChargeTime: 30 Description: Invulnerability @@ -129,8 +129,9 @@ FTUR: BeginChargeSound: ironchg1.aud EndChargeSound: ironrdy1.aud Range: 1 - Upgrades: invulnerability - GrantUpgradeSequence: idle + Condition: invulnerability + Sequence: idle + OnFireSound: ironcur9.aud Power: Amount: 0 @@ -186,7 +187,7 @@ T17: Duration: 999999 KillCargo: yes Range: 3 - GrantUpgradePower@IRONCURTAIN: + GrantExternalConditionPower@IRONCURTAIN: Icon: invuln ChargeTime: 30 Description: Invulnerability @@ -196,5 +197,6 @@ T17: BeginChargeSound: ironchg1.aud EndChargeSound: ironrdy1.aud Range: 1 - Upgrades: invulnerability - GrantUpgradeSequence: idle + Condition: invulnerability + Sequence: idle + OnFireSound: ironcur9.aud diff --git a/mods/ra/maps/fort-lonestar/rules.yaml b/mods/ra/maps/fort-lonestar/rules.yaml index 22def065b8..3ac6f9f0f0 100644 --- a/mods/ra/maps/fort-lonestar/rules.yaml +++ b/mods/ra/maps/fort-lonestar/rules.yaml @@ -69,11 +69,11 @@ FORTCRATE: GiveUnitCrateAction@e7: Units: e7 SelectionShares: 10 - GrantUpgradeCrateAction@ironcurtain: + GrantExternalConditionCrateAction@ironcurtain: SelectionShares: 10 Effect: invuln Notification: ironcur9.aud - Upgrades: invulnerability + Condition: invulnerability Duration: 1200 ExplodeCrateAction@bigboom: Weapon: SCUD diff --git a/mods/ra/rules/defaults.yaml b/mods/ra/rules/defaults.yaml index d7ad8b18c0..f6347c96af 100644 --- a/mods/ra/rules/defaults.yaml +++ b/mods/ra/rules/defaults.yaml @@ -15,7 +15,7 @@ ^GainsExperience: GainsExperience: - Upgrades: + Upgrades: 200: rank-veteran-1 400: rank-veteran-2 800: rank-veteran-3 @@ -121,8 +121,10 @@ DamageMultiplier@IRONCURTAIN: RequiresCondition: invulnerability Modifier: 0 - TimedUpgradeBar: - Upgrade: invulnerability + TimedConditionBar: + Condition: invulnerability + ExternalConditions@INVULNERABILITY: + Conditions: invulnerability ^Vehicle: Inherits@1: ^ExistsInWorld diff --git a/mods/ra/rules/misc.yaml b/mods/ra/rules/misc.yaml index d0e626927c..6054918c4c 100644 --- a/mods/ra/rules/misc.yaml +++ b/mods/ra/rules/misc.yaml @@ -90,11 +90,11 @@ CRATE: Units: e1,e1,e4,e4,e3,e3,e3 ValidFactions: soviet, russia, ukraine TimeDelay: 4500 - GrantUpgradeCrateAction@invuln: + GrantExternalConditionCrateAction@invuln: SelectionShares: 5 Effect: invuln Notification: ironcur9.aud - Upgrades: invulnerability + Condition: invulnerability Duration: 600 MONEYCRATE: diff --git a/mods/ra/rules/structures.yaml b/mods/ra/rules/structures.yaml index 58730edadc..d485a486c2 100644 --- a/mods/ra/rules/structures.yaml +++ b/mods/ra/rules/structures.yaml @@ -303,7 +303,7 @@ IRON: Range: 10c0 Bib: HasMinibib: Yes - GrantUpgradePower@IRONCURTAIN: + GrantExternalConditionPower@IRONCURTAIN: Icon: invuln ChargeTime: 120 Description: Invulnerability @@ -314,7 +314,8 @@ IRON: BeginChargeSpeechNotification: IronCurtainCharging EndChargeSpeechNotification: IronCurtainReady DisplayRadarPing: True - Upgrades: invulnerability + Condition: invulnerability + OnFireSound: ironcur9.aud SupportPowerChargeBar: Power: Amount: -200 diff --git a/mods/ts/rules/civilian-vehicles.yaml b/mods/ts/rules/civilian-vehicles.yaml index 75c53f476c..f43065b380 100644 --- a/mods/ts/rules/civilian-vehicles.yaml +++ b/mods/ts/rules/civilian-vehicles.yaml @@ -102,7 +102,7 @@ BUS: MaxWeight: 20 PipCount: 5 UnloadVoice: Unload - LoadingUpgrades: notmobile + LoadingUpgrades: loading EjectOnDeath: true PICK: @@ -126,7 +126,7 @@ PICK: MaxWeight: 2 PipCount: 5 UnloadVoice: Unload - LoadingUpgrades: notmobile + LoadingUpgrades: loading EjectOnDeath: true CAR: @@ -150,7 +150,7 @@ CAR: MaxWeight: 4 PipCount: 5 UnloadVoice: Unload - LoadingUpgrades: notmobile + LoadingUpgrades: loading EjectOnDeath: true WINI: @@ -174,7 +174,7 @@ WINI: MaxWeight: 5 PipCount: 5 UnloadVoice: Unload - LoadingUpgrades: notmobile + LoadingUpgrades: loading EjectOnDeath: true LOCOMOTIVE: diff --git a/mods/ts/rules/defaults.yaml b/mods/ts/rules/defaults.yaml index 679010a8d5..c3d01b1bc5 100644 --- a/mods/ts/rules/defaults.yaml +++ b/mods/ts/rules/defaults.yaml @@ -64,6 +64,8 @@ ReferencePoint: Bottom, Right RequiresCondition: rank-elite ZOffset: 256 + ExternalConditions@CRATES: + Conditions: crate-firepower, crate-damage, crate-speed, crate-cloak ^EmpDisable: UpgradeOverlay@EMPDISABLE: @@ -71,8 +73,8 @@ Palette: disabled DisableOnUpgrade@EMPDISABLE: RequiresCondition: empdisable - TimedUpgradeBar@EMPDISABLE: - Upgrade: empdisable + TimedConditionBar@EMPDISABLE: + Condition: empdisable Color: FFFFFF WithIdleOverlay@EMPDISABLE: Sequence: emp-overlay @@ -83,11 +85,13 @@ PowerMultiplier@EMPDISABLE: RequiresCondition: empdisable Modifier: 0 + ExternalConditions@EMPDISABLE: + Conditions: empdisable ^EmpDisableMobile: Inherits: ^EmpDisable Mobile: - RequiresCondition: !notmobile + RequiresCondition: !empdisable && !deployed && !loading ^Cloakable: Cloak@CLOAKGENERATOR: @@ -639,7 +643,6 @@ Mobile: Speed: 113 TurnSpeed: 16 - Crushes: crate SharesCell: no TerrainSpeeds: Clear: 90 @@ -795,7 +798,7 @@ Cargo: Types: Infantry UnloadVoice: Unload - LoadingUpgrades: notmobile + LoadingUpgrades: loading Health: HP: 100 Armor: diff --git a/mods/ts/rules/gdi-vehicles.yaml b/mods/ts/rules/gdi-vehicles.yaml index 86c58f6dee..e57ea36dbb 100644 --- a/mods/ts/rules/gdi-vehicles.yaml +++ b/mods/ts/rules/gdi-vehicles.yaml @@ -28,7 +28,7 @@ APC: MaxWeight: 5 PipCount: 5 UnloadVoice: Unload - LoadingUpgrades: notmobile + LoadingUpgrades: loading EjectOnDeath: true UpgradeOnTerrain: Upgrades: inwater diff --git a/mods/ts/rules/misc.yaml b/mods/ts/rules/misc.yaml index aa176c15f2..45d14be609 100644 --- a/mods/ts/rules/misc.yaml +++ b/mods/ts/rules/misc.yaml @@ -74,24 +74,24 @@ CRATE: SelectionShares: 0 NoBaseSelectionShares: 100 Units: mcv - GrantUpgradeCrateAction@cloak: + GrantExternalConditionCrateAction@cloak: SelectionShares: 5 Effect: stealth - Upgrades: crate-cloak + Condition: crate-cloak Notification: cloak5.aud - GrantUpgradeCrateAction@firepower: + GrantExternalConditionCrateAction@firepower: SelectionShares: 5 Effect: firepower - Upgrades: crate-firepower + Condition: crate-firepower Notification: 00-i070.aud - GrantUpgradeCrateAction@armor: + GrantExternalConditionCrateAction@armor: SelectionShares: 5 Effect: armor - Upgrades: crate-damage + Condition: crate-damage Notification: 00-i068.aud - GrantUpgradeCrateAction@speed: + GrantExternalConditionCrateAction@speed: SelectionShares: 5 - Upgrades: crate-speed + Condition: crate-speed Notification: 00-i080.aud SROCK01: diff --git a/mods/ts/rules/nod-vehicles.yaml b/mods/ts/rules/nod-vehicles.yaml index 8837a3402e..965ede8f7f 100644 --- a/mods/ts/rules/nod-vehicles.yaml +++ b/mods/ts/rules/nod-vehicles.yaml @@ -106,7 +106,7 @@ TTNK: RenderSprites: Image: ttnk DeployToUpgrade: - DeployedUpgrades: deployed, notmobile + DeployedUpgrades: deployed UndeployedUpgrades: undeployed DeployAnimation: make Facing: 160 @@ -285,7 +285,7 @@ SAPC: MaxWeight: 5 PipCount: 5 UnloadVoice: Unload - LoadingUpgrades: notmobile + LoadingUpgrades: loading EjectOnDeath: true SUBTANK: diff --git a/mods/ts/rules/shared-vehicles.yaml b/mods/ts/rules/shared-vehicles.yaml index b90046afa5..d66be951c3 100644 --- a/mods/ts/rules/shared-vehicles.yaml +++ b/mods/ts/rules/shared-vehicles.yaml @@ -138,7 +138,7 @@ LPST: gdi: lpst.gdi nod: lpst.nod DeployToUpgrade: - DeployedUpgrades: deployed, notmobile + DeployedUpgrades: deployed UndeployedUpgrades: undeployed DeployAnimation: make Facing: 160 diff --git a/mods/ts/weapons/superweapons.yaml b/mods/ts/weapons/superweapons.yaml index 7c9f6fdf9d..f85b9a7802 100644 --- a/mods/ts/weapons/superweapons.yaml +++ b/mods/ts/weapons/superweapons.yaml @@ -107,10 +107,10 @@ EMPulseCannon: Image: pulsball Warhead@1Eff: CreateEffect Explosions: pulse_explosion - Warhead@emp: GrantUpgrade + Warhead@emp: GrantExternalCondition Range: 4c0 Duration: 250 - Upgrades: empdisable, notmobile + Condition: empdisable ClusterMissile: ValidTargets: Ground, Water, Air