#region Copyright & License Information /* * Copyright 2007-2017 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.Primitives; 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 conditions by warheads, experience, crates, support powers, etc.")] public class ConditionManagerInfo : TraitInfo, Requires { } public class ConditionManager : INotifyCreated, ITick { /// Value used to represent an invalid token. public static readonly int InvalidConditionToken = -1; class ConditionTimer { public readonly int Token; public readonly int Duration; public int Remaining; public ConditionTimer(int token, int duration) { Token = token; Duration = Remaining = duration; } } class ConditionState { /// Traits that have registered to be notified when this condition changes. public readonly List Consumers = new List(); /// Unique integers identifying granted instances of the condition. public readonly HashSet Tokens = new HashSet(); /// External callbacks that are to be executed when a timed condition changes. public readonly List Watchers = 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(); int nextToken = 1; /// Cache of condition -> enabled state for quick evaluation of token counter conditions. readonly Dictionary conditionCache = new Dictionary(); /// Read-only version of conditionCache that is passed to IConditionConsumers. IReadOnlyDictionary readOnlyConditionCache; void INotifyCreated.Created(Actor self) { state = new Dictionary(); 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) { var cs = state.GetOrAdd(condition); cs.Consumers.Add(consumer); foreach (var w in allWatchers) if (w.Condition == condition) cs.Watchers.Add(w); conditionCache[condition] = 0; } } // Enable any conditions granted during trait setup foreach (var kv in tokens) { ConditionState conditionState; if (!state.TryGetValue(kv.Value, out conditionState)) continue; conditionState.Tokens.Add(kv.Key); conditionCache[kv.Value] = conditionState.Tokens.Count; } // Update all traits with their initial condition state foreach (var consumer in allConsumers) consumer.ConditionsChanged(self, readOnlyConditionCache); } void UpdateConditionState(Actor self, string condition, int token, bool isRevoke) { ConditionState conditionState; if (!state.TryGetValue(condition, out conditionState)) return; if (isRevoke) conditionState.Tokens.Remove(token); else conditionState.Tokens.Add(token); conditionCache[condition] = conditionState.Tokens.Count; foreach (var t in conditionState.Consumers) t.ConditionsChanged(self, readOnlyConditionCache); } /// Grants a specified condition. /// The token that is used to revoke this condition. /// Validate against the external condition whitelist. /// Automatically revoke condition after this delay if non-zero. public int GrantCondition(Actor self, string condition, int duration = 0) { 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) UpdateConditionState(self, condition, token, false); return token; } /// Revokes a previously granted condition. /// The invalid token ID. /// The token ID returned by GrantCondition. public int RevokeCondition(Actor self, int token) { string condition; if (!tokens.TryGetValue(token, out condition)) throw new InvalidOperationException("Attempting to revoke condition with invalid token {0} for {1}.".F(token, self)); 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); return InvalidConditionToken; } /// 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(); } } }