Rewrite UpgradeManager implementation to suit conditions.

This commit is contained in:
Paul Chote
2016-11-19 16:18:06 +00:00
parent 05187f5828
commit 3f0b33992c
17 changed files with 219 additions and 160 deletions

View File

@@ -12,49 +12,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[Desc("Attach this to a unit to enable dynamic upgrades by warheads, experience, crates, support powers, etc.")]
public class UpgradeManagerInfo : TraitInfo<UpgradeManager>, IRulesetLoaded
{
public void RulesetLoaded(Ruleset rules, ActorInfo info)
{
if (!info.Name.StartsWith("^") && !info.TraitInfos<IUpgradableInfo>().Any())
throw new YamlException(
"There are no upgrades to be managed for actor '{0}'. You are either missing some upgradeable traits, or this UpgradeManager trait is not required.".F(
info.Name));
}
}
public class UpgradeManagerInfo : TraitInfo<UpgradeManager>, Requires<IConditionConsumerInfo> { }
public class UpgradeManager : INotifyCreated, ITick
{
class TimedUpgrade
/// <summary>Value used to represent an invalid token.</summary>
public static readonly int InvalidConditionToken = -1;
class TimedCondition
{
public class UpgradeSource
public class ConditionSource
{
public readonly object Source;
public int Remaining;
public UpgradeSource(int duration, object source)
public ConditionSource(int duration, object source)
{
Remaining = duration;
Source = source;
}
}
public readonly string Upgrade;
public readonly string Condition;
public readonly int Duration;
public readonly HashSet<UpgradeSource> Sources;
public readonly HashSet<ConditionSource> Sources;
public int Remaining; // Equal to maximum of all Sources.Remaining
public TimedUpgrade(string upgrade, int duration, object source)
public TimedCondition(string condition, int duration, object source)
{
Upgrade = upgrade;
Condition = condition;
Duration = duration;
Remaining = duration;
Sources = new HashSet<UpgradeSource> { new UpgradeSource(duration, source) };
Sources = new HashSet<ConditionSource> { new ConditionSource(duration, source) };
}
public void Tick()
@@ -65,29 +60,124 @@ namespace OpenRA.Mods.Common.Traits
}
}
class UpgradeState
class ConditionState
{
public readonly List<IUpgradable> Traits = new List<IUpgradable>();
public readonly List<object> Sources = new List<object>();
/// <summary>Traits that have registered to be notified when this condition changes.</summary>
public readonly List<IConditionConsumer> Consumers = new List<IConditionConsumer>();
/// <summary>Unique integers identifying granted instances of the condition.</summary>
public readonly HashSet<int> Tokens = new HashSet<int>();
/// <summary>External callbacks that are to be executed when a timed condition changes.</summary>
public readonly List<Action<int, int>> Watchers = new List<Action<int, int>>();
}
readonly List<TimedUpgrade> timedUpgrades = new List<TimedUpgrade>();
readonly Dictionary<IUpgradable, int> levels = new Dictionary<IUpgradable, int>();
Dictionary<string, UpgradeState> upgrades;
readonly List<TimedCondition> timedConditions = new List<TimedCondition>();
Dictionary<string, ConditionState> state;
/// <summary>Each granted condition receives a unique token that is used when revoking.</summary>
Dictionary<int, string> tokens = new Dictionary<int, string>();
int nextToken = 1;
/// <summary>Temporary shim between the old and new upgrade/condition grant and revoke methods.</summary>
Dictionary<Pair<object, string>, int> objectTokenShim = new Dictionary<Pair<object, string>, int>();
/// <summary>Cache of condition -> enabled state for quick evaluation of boolean conditions.</summary>
Dictionary<string, bool> conditionCache = new Dictionary<string, bool>();
/// <summary>Read-only version of conditionCache that is passed to IConditionConsumers.</summary>
IReadOnlyDictionary<string, bool> readOnlyConditionCache;
void INotifyCreated.Created(Actor self)
{
upgrades = new Dictionary<string, UpgradeState>();
foreach (var up in self.TraitsImplementing<IUpgradable>())
foreach (var t in up.UpgradeTypes)
upgrades.GetOrAdd(t).Traits.Add(up);
state = new Dictionary<string, ConditionState>();
readOnlyConditionCache = new ReadOnlyDictionary<string, bool>(conditionCache);
var allConsumers = new HashSet<IConditionConsumer>();
foreach (var consumer in self.TraitsImplementing<IConditionConsumer>())
{
allConsumers.Add(consumer);
foreach (var condition in consumer.Conditions)
{
state.GetOrAdd(condition).Consumers.Add(consumer);
conditionCache[condition] = false;
}
}
// 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 > 0;
}
// Update all traits with their initial condition state
foreach (var consumer in allConsumers)
consumer.ConditionsChanged(self, readOnlyConditionCache);
}
void CheckCanManageUpgrades()
void UpdateConditionState(Actor self, string condition, int token, bool isRevoke)
{
if (upgrades == null)
throw new InvalidOperationException("Upgrades cannot be managed until the actor has been fully created.");
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 > 0;
foreach (var t in conditionState.Consumers)
t.ConditionsChanged(self, readOnlyConditionCache);
}
/// <summary>Grants a specified condition.</summary>
/// <returns>The token that is used to revoke this condition.</returns>
public int GrantCondition(Actor self, string condition)
{
var token = nextToken++;
tokens.Add(token, condition);
// 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;
}
/// <summary>Revokes a previously granted condition.</summary>
/// <returns>The invalid token ID</returns>
/// <param name="token">The token ID returned by GrantCondition.</param>
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);
// Conditions may be granted and revoked before the state is initialized.
if (state != null)
UpdateConditionState(self, condition, token, true);
return InvalidConditionToken;
}
#region Shim methods for legacy upgrade granting code
void CheckCanManageConditions()
{
if (state == null)
throw new InvalidOperationException("Conditions cannot be managed until the actor has been fully created.");
}
/// <summary>Upgrade level increments are limited to dupesAllowed per source, i.e., if a single
@@ -98,11 +188,11 @@ namespace OpenRA.Mods.Common.Traits
/// remaining upgrade duration will be replaced by the new source.</summary>
public void GrantTimedUpgrade(Actor self, string upgrade, int duration, object source = null, int dupesAllowed = 1)
{
var timed = timedUpgrades.FirstOrDefault(u => u.Upgrade == upgrade);
var timed = timedConditions.FirstOrDefault(u => u.Condition == upgrade);
if (timed == null)
{
timed = new TimedUpgrade(upgrade, duration, source);
timedUpgrades.Add(timed);
timed = new TimedCondition(upgrade, duration, source);
timedConditions.Add(timed);
GrantUpgrade(self, upgrade, timed);
return;
}
@@ -110,7 +200,7 @@ namespace OpenRA.Mods.Common.Traits
var srcs = timed.Sources.Where(s => s.Source == source);
if (srcs.Count() < dupesAllowed)
{
timed.Sources.Add(new TimedUpgrade.UpgradeSource(duration, source));
timed.Sources.Add(new TimedCondition.ConditionSource(duration, source));
if (AcceptsUpgrade(self, upgrade))
GrantUpgrade(self, upgrade, timed);
else
@@ -122,108 +212,69 @@ namespace OpenRA.Mods.Common.Traits
timed.Remaining = Math.Max(duration, timed.Remaining);
}
// Different upgradeable traits may define (a) different level ranges for the same upgrade type,
// and (b) multiple upgrade types for the same trait. The unrestricted level for each trait is
// tracked independently so that we can correctly revoke levels without adding the burden of
// tracking both the overall (unclamped) and effective (clamped) levels on each individual trait.
void NotifyUpgradeLevelChanged(IEnumerable<IUpgradable> traits, Actor self, string upgrade, int levelAdjust)
{
foreach (var up in traits)
{
var oldLevel = levels.GetOrAdd(up);
var newLevel = levels[up] = oldLevel + levelAdjust;
// This will internally clamp the levels to its own restricted range
up.UpgradeLevelChanged(self, upgrade, oldLevel, newLevel);
}
}
int GetOverallLevel(IUpgradable upgradable)
{
int level;
return levels.TryGetValue(upgradable, out level) ? level : 0;
}
public void GrantUpgrade(Actor self, string upgrade, object source)
{
CheckCanManageUpgrades();
UpgradeState s;
if (!upgrades.TryGetValue(upgrade, out s))
return;
// Track the upgrade source so that the upgrade can be removed without conflicts
s.Sources.Add(source);
NotifyUpgradeLevelChanged(s.Traits, self, upgrade, 1);
CheckCanManageConditions();
objectTokenShim[Pair.New(source, upgrade)] = GrantCondition(self, upgrade);
}
public void RevokeUpgrade(Actor self, string upgrade, object source)
{
CheckCanManageUpgrades();
UpgradeState s;
if (!upgrades.TryGetValue(upgrade, out s))
return;
if (!s.Sources.Remove(source))
throw new InvalidOperationException("Object <{0}> revoked more levels of upgrade {1} than it granted for {2}.".F(source, upgrade, self));
NotifyUpgradeLevelChanged(s.Traits, self, upgrade, -1);
CheckCanManageConditions();
RevokeCondition(self, objectTokenShim[Pair.New(source, upgrade)]);
}
/// <summary>Returns true if the actor uses the given upgrade. Does not check the actual level of the upgrade.</summary>
public bool AcknowledgesUpgrade(Actor self, string upgrade)
{
CheckCanManageUpgrades();
return upgrades.ContainsKey(upgrade);
CheckCanManageConditions();
return state.ContainsKey(upgrade);
}
/// <summary>Returns true only if the actor can accept another level of the upgrade.</summary>
public bool AcceptsUpgrade(Actor self, string upgrade)
{
CheckCanManageUpgrades();
UpgradeState s;
if (!upgrades.TryGetValue(upgrade, out s))
CheckCanManageConditions();
bool enabled;
if (!conditionCache.TryGetValue(upgrade, out enabled))
return false;
return s.Traits.Any(up => up.AcceptsUpgradeLevel(self, upgrade, GetOverallLevel(up) + 1));
return !enabled;
}
public void RegisterWatcher(string upgrade, Action<int, int> action)
{
CheckCanManageUpgrades();
CheckCanManageConditions();
UpgradeState s;
if (!upgrades.TryGetValue(upgrade, out s))
ConditionState s;
if (!state.TryGetValue(upgrade, out s))
return;
s.Watchers.Add(action);
}
/// <summary>Watchers will be receiving notifications while the upgrade's level is nonzero.
/// They will also be provided with the number of ticks before the level returns to zero,
/// <summary>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).</summary>
public void Tick(Actor self)
void ITick.Tick(Actor self)
{
CheckCanManageUpgrades();
foreach (var u in timedUpgrades)
foreach (var u in timedConditions)
{
u.Tick();
foreach (var source in u.Sources)
if (source.Remaining <= 0)
RevokeUpgrade(self, u.Upgrade, u);
RevokeUpgrade(self, u.Condition, u);
u.Sources.RemoveWhere(source => source.Remaining <= 0);
foreach (var a in upgrades[u.Upgrade].Watchers)
foreach (var a in state[u.Condition].Watchers)
a(u.Duration, u.Remaining);
}
timedUpgrades.RemoveAll(u => u.Remaining <= 0);
timedConditions.RemoveAll(u => u.Remaining <= 0);
}
#endregion
}
}