Merge pull request #12796 from pchote/external-conditions-rework

Add support for per-source and total external condition caps.
This commit is contained in:
reaperrr
2017-02-19 15:44:53 +01:00
committed by GitHub
20 changed files with 266 additions and 135 deletions

View File

@@ -777,7 +777,7 @@
<Compile Include="Traits\AutoCarryall.cs" />
<Compile Include="Traits\World\CliffBackImpassabilityLayer.cs" />
<Compile Include="Traits\Conditions\GrantCondition.cs" />
<Compile Include="Traits\Conditions\ExternalConditions.cs" />
<Compile Include="Traits\Conditions\ExternalCondition.cs" />
<Compile Include="Traits\Conditions\StackedCondition.cs" />
<Compile Include="Traits\Buildings\BridgeHut.cs" />
<Compile Include="Traits\Buildings\BridgePlaceholder.cs" />

View File

@@ -20,15 +20,16 @@ using OpenRA.Traits;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptPropertyGroup("General")]
public class ConditionProperties : ScriptActorProperties, Requires<ConditionManagerInfo>
public class ConditionProperties : ScriptActorProperties, Requires<ExternalConditionInfo>
{
readonly ConditionManager conditionManager;
readonly ExternalCondition[] externalConditions;
readonly Dictionary<string, Stack<int>> legacyShim = new Dictionary<string, Stack<int>>();
public ConditionProperties(ScriptContext context, Actor self)
: base(context, self)
{
conditionManager = self.Trait<ConditionManager>();
externalConditions = self.TraitsImplementing<ExternalCondition>().ToArray();
}
[Desc("Grant an external condition on this actor and return the revocation token.",
@@ -36,22 +37,27 @@ namespace OpenRA.Mods.Common.Scripting
"If duration > 0 the condition will be automatically revoked after the defined number of ticks")]
public int GrantCondition(string condition, int duration = 0)
{
if (!conditionManager.AcceptsExternalCondition(Self, condition, duration > 0))
throw new InvalidDataException("Condition `{0}` has not been listed on an ExternalConditions trait".F(condition));
var external = externalConditions
.FirstOrDefault(t => t.Info.Condition == condition && t.CanGrantCondition(Self, this));
return conditionManager.GrantCondition(Self, condition, true, duration);
if (external == null)
throw new InvalidDataException("Condition `{0}` has not been listed on an enabled ExternalCondition trait".F(condition));
return external.GrantCondition(Self, this, duration);
}
[Desc("Revoke a condition using the token returned by GrantCondition.")]
public void RevokeCondition(int token)
{
conditionManager.RevokeCondition(Self, token);
foreach (var external in externalConditions)
external.TryRevokeCondition(Self, this, token);
}
[Desc("Check whether this actor accepts a specific external condition.")]
public bool AcceptsCondition(string condition, bool timed = false)
public bool AcceptsCondition(string condition)
{
return conditionManager.AcceptsExternalCondition(Self, condition, timed);
return externalConditions
.Any(t => t.Info.Condition == condition && t.CanGrantCondition(Self, this));
}
[Desc("Grant an upgrade to this actor. DEPRECATED! Will be removed.")]

View File

@@ -63,9 +63,6 @@ namespace OpenRA.Mods.Common.Traits
/// <summary>Each granted condition receives a unique token that is used when revoking.</summary>
Dictionary<int, string> tokens = new Dictionary<int, string>();
/// <summary>Set of whitelisted externally grantable conditions cached from ExternalConditions traits.</summary>
string[] externalConditions = { };
/// <summary>Set of conditions that are monitored for stacked bonuses, and the bonus conditions that they grant.</summary>
readonly Dictionary<string, string[]> stackedConditions = new Dictionary<string, string[]>();
@@ -114,12 +111,6 @@ namespace OpenRA.Mods.Common.Traits
conditionCache[kv.Value] = conditionState.Tokens.Count > 0;
}
// Build external condition whitelist
externalConditions = self.Info.TraitInfos<ExternalConditionsInfo>()
.SelectMany(t => t.Conditions)
.Distinct()
.ToArray();
foreach (var sc in self.Info.TraitInfos<StackedConditionInfo>())
{
stackedConditions[sc.Condition] = sc.StackedConditions;
@@ -172,11 +163,8 @@ namespace OpenRA.Mods.Common.Traits
/// <returns>The token that is used to revoke this condition.</returns>
/// <param name="external">Validate against the external condition whitelist.</param>
/// <param name="duration">Automatically revoke condition after this delay if non-zero.</param>
public int GrantCondition(Actor self, string condition, bool external = false, int duration = 0)
public int GrantCondition(Actor self, string condition, int duration = 0)
{
if (external && !externalConditions.Contains(condition))
return InvalidConditionToken;
var token = nextToken++;
tokens.Add(token, condition);
@@ -218,26 +206,6 @@ namespace OpenRA.Mods.Common.Traits
return InvalidConditionToken;
}
/// <summary>Returns true if the given external condition will have an effect on this actor.</summary>
public bool AcceptsExternalCondition(Actor self, string condition, bool timed = false)
{
if (state == null)
throw new InvalidOperationException("AcceptsExternalCondition cannot be queried before the actor has been fully created.");
if (!externalConditions.Contains(condition))
return false;
// A timed condition can always replace an existing timed condition (resetting its duration)
if (timed && timers.ContainsKey(condition))
return true;
string[] sc;
if (stackedConditions.TryGetValue(condition, out sc))
return stackedTokens[condition].Count < sc.Length;
return !conditionCache[condition];
}
/// <summary>Returns whether the specified token is valid for RevokeCondition</summary>
public bool TokenValid(Actor self, int token)
{

View File

@@ -0,0 +1,142 @@
#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.Traits;
namespace OpenRA.Mods.Common.Traits
{
[Desc("Allows a condition to be granted from an external source (Lua, warheads, etc).")]
public class ExternalConditionInfo : ITraitInfo, Requires<ConditionManagerInfo>
{
[GrantedConditionReference]
[FieldLoader.Require]
public readonly string Condition = null;
[Desc("If > 0, restrict the number of times that this condition can be granted by a single source.")]
public readonly int SourceCap = 0;
[Desc("If > 0, restrict the number of times that this condition can be granted by any source.")]
public readonly int TotalCap = 0;
public object Create(ActorInitializer init) { return new ExternalCondition(init.Self, this); }
}
public class ExternalCondition
{
class TimedToken
{
public int Token;
public int Expires;
}
public readonly ExternalConditionInfo Info;
readonly ConditionManager conditionManager;
readonly Dictionary<object, HashSet<int>> permanentTokens = new Dictionary<object, HashSet<int>>();
readonly Dictionary<object, HashSet<TimedToken>> timedTokens = new Dictionary<object, HashSet<TimedToken>>();
public ExternalCondition(Actor self, ExternalConditionInfo info)
{
Info = info;
conditionManager = self.Trait<ConditionManager>();
}
public bool CanGrantCondition(Actor self, object source)
{
if (conditionManager == null || source == null)
return false;
// Timed tokens do not count towards the source cap: the condition with the shortest
// remaining duration can always be revoked to make room.
if (Info.SourceCap > 0 && permanentTokens.GetOrAdd(source).Count >= Info.SourceCap)
return false;
if (Info.TotalCap > 0 && permanentTokens.Values.SelectMany(t => t).Count() >= Info.TotalCap)
return false;
return true;
}
public int GrantCondition(Actor self, object source, int duration = 0)
{
if (conditionManager == null || source == null || !CanGrantCondition(self, source))
return ConditionManager.InvalidConditionToken;
var token = conditionManager.GrantCondition(self, Info.Condition, duration);
var permanent = permanentTokens.GetOrAdd(source);
if (duration > 0)
{
var timed = timedTokens.GetOrAdd(source);
// Remove expired tokens
timed.RemoveWhere(t => t.Expires < self.World.WorldTick);
// Check level caps
if (Info.SourceCap > 0)
{
if (permanent.Count + timed.Count >= Info.SourceCap)
{
var expire = timed.MinByOrDefault(t => t.Expires);
if (expire != null)
{
timed.Remove(expire);
if (conditionManager.TokenValid(self, expire.Token))
conditionManager.RevokeCondition(self, expire.Token);
}
}
}
if (Info.TotalCap > 0)
{
var totalCount = permanentTokens.Values.SelectMany(t => t).Count() + timedTokens.Values.SelectMany(t => t).Count();
if (totalCount >= Info.TotalCap)
{
// Prefer tokens from the same source
var expire = timedTokens.SelectMany(t => t.Value.Select(tt => new Tuple<object, TimedToken>(t.Key, tt)))
.MinByOrDefault(t => t.Item2.Expires);
if (expire != null)
{
if (conditionManager.TokenValid(self, expire.Item2.Token))
conditionManager.RevokeCondition(self, expire.Item2.Token);
timedTokens[expire.Item1].Remove(expire.Item2);
}
}
}
timed.Add(new TimedToken { Expires = self.World.WorldTick + duration, Token = token });
}
else
permanent.Add(token);
return token;
}
/// <summary>Revokes the external condition with the given token if it was granted by this trait.</summary>
/// <returns><c>true</c> if the now-revoked condition was originally granted by this trait.</returns>
public bool TryRevokeCondition(Actor self, object source, int token)
{
if (conditionManager == null || source == null)
return false;
var removed = permanentTokens.GetOrAdd(source).Remove(token) ||
timedTokens.GetOrAdd(source).RemoveWhere(t => t.Token == token) > 0;
if (removed && conditionManager.TokenValid(self, token))
conditionManager.RevokeCondition(self, token);
return true;
}
}
}

View File

@@ -1,25 +0,0 @@
#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 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<ExternalConditions>
{
[GrantedConditionReference]
public readonly string[] Conditions = { };
}
public class ExternalConditions { }
}

View File

@@ -10,6 +10,7 @@
#endregion
using System.Collections.Generic;
using System.Linq;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
@@ -104,13 +105,18 @@ namespace OpenRA.Mods.Common.Traits
if (a == self && !info.AffectsParent)
return;
if (tokens.ContainsKey(a))
return;
var stance = self.Owner.Stances[a.Owner];
if (!info.ValidStances.HasStance(stance))
return;
var cm = a.TraitOrDefault<ConditionManager>();
if (cm != null && !tokens.ContainsKey(a) && cm.AcceptsExternalCondition(a, info.Condition))
tokens[a] = cm.GrantCondition(a, info.Condition, true);
var external = a.TraitsImplementing<ExternalCondition>()
.FirstOrDefault(t => t.Info.Condition == info.Condition && t.CanGrantCondition(a, self));
if (external != null)
tokens[a] = external.GrantCondition(a, self);
}
public void UnitProducedByOther(Actor self, Actor producer, Actor produced)
@@ -130,9 +136,11 @@ namespace OpenRA.Mods.Common.Traits
if (!info.ValidStances.HasStance(stance))
return;
var cm = produced.TraitOrDefault<ConditionManager>();
if (cm != null && cm.AcceptsExternalCondition(produced, info.Condition))
tokens[produced] = cm.GrantCondition(produced, info.Condition, true);
var external = produced.TraitsImplementing<ExternalCondition>()
.FirstOrDefault(t => t.Info.Condition == info.Condition && t.CanGrantCondition(produced, self));
if (external != null)
tokens[produced] = external.GrantCondition(produced, self);
}
}
@@ -146,9 +154,8 @@ namespace OpenRA.Mods.Common.Traits
return;
tokens.Remove(a);
var cm = a.TraitOrDefault<ConditionManager>();
if (cm != null)
cm.RevokeCondition(a, token);
foreach (var external in a.TraitsImplementing<ExternalCondition>())
external.TryRevokeCondition(a, self, token);
}
}
}

View File

@@ -47,8 +47,8 @@ namespace OpenRA.Mods.Common.Traits
bool AcceptsCondition(Actor a)
{
var cm = a.TraitOrDefault<ConditionManager>();
return cm != null && cm.AcceptsExternalCondition(a, info.Condition, info.Duration > 0);
return a.TraitsImplementing<ExternalCondition>()
.Any(t => t.Info.Condition == info.Condition && t.CanGrantCondition(a, self));
}
public override int GetSelectionShares(Actor collector)
@@ -71,11 +71,11 @@ namespace OpenRA.Mods.Common.Traits
if (!a.IsInWorld || a.IsDead)
continue;
var cm = a.TraitOrDefault<ConditionManager>();
var external = a.TraitsImplementing<ExternalCondition>()
.FirstOrDefault(t => t.Info.Condition == info.Condition && t.CanGrantCondition(a, self));
// Condition token is ignored because we never revoke this condition.
if (cm != null)
cm.GrantCondition(a, info.Condition, true, info.Duration);
if (external != null)
external.GrantCondition(a, self, info.Duration);
}
});

View File

@@ -69,11 +69,11 @@ namespace OpenRA.Mods.Common.Traits
foreach (var a in UnitsInRange(order.TargetLocation))
{
var cm = a.TraitOrDefault<ConditionManager>();
var external = a.TraitsImplementing<ExternalCondition>()
.FirstOrDefault(t => t.Info.Condition == info.Condition && t.CanGrantCondition(a, self));
// Condition token is ignored because we never revoke this condition.
if (cm != null)
cm.GrantCondition(a, info.Condition, true, info.Duration);
if (external != null)
external.GrantCondition(a, self, info.Duration);
}
}
@@ -90,8 +90,8 @@ namespace OpenRA.Mods.Common.Traits
if (!a.Owner.IsAlliedWith(Self.Owner))
return false;
var cm = a.TraitOrDefault<ConditionManager>();
return cm != null && cm.AcceptsExternalCondition(a, info.Condition, info.Duration > 0);
return a.TraitsImplementing<ExternalCondition>()
.Any(t => t.Info.Condition == info.Condition && t.CanGrantCondition(a, Self));
});
}

View File

@@ -850,6 +850,24 @@ namespace OpenRA.Mods.Common.UtilityCommands
}
}
if (engineVersion < 20170218)
{
var externalConditions = node.Value.Nodes.Where(n => n.Key.StartsWith("ExternalConditions", StringComparison.Ordinal));
foreach (var ec in externalConditions.ToList())
{
var conditionsNode = ec.Value.Nodes.FirstOrDefault(n => n.Key == "Conditions");
if (conditionsNode != null)
{
var conditions = FieldLoader.GetValue<string[]>("", conditionsNode.Value.Value);
foreach (var c in conditions)
node.Value.Nodes.Add(new MiniYamlNode("ExternalCondition@" + c.ToUpperInvariant(),
new MiniYaml("", new List<MiniYamlNode>() { new MiniYamlNode("Condition", c) })));
node.Value.Nodes.Remove(ec);
}
}
}
UpgradeActorRules(modData, engineVersion, ref node.Value.Nodes, node, depth + 1);
}

View File

@@ -10,6 +10,7 @@
#endregion
using System.Collections.Generic;
using System.Linq;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
@@ -36,11 +37,11 @@ namespace OpenRA.Mods.Common.Warheads
if (!IsValidAgainst(a, firedBy))
continue;
var cm = a.TraitOrDefault<ConditionManager>();
var external = a.TraitsImplementing<ExternalCondition>()
.FirstOrDefault(t => t.Info.Condition == Condition && t.CanGrantCondition(a, firedBy));
// Condition token is ignored because we never revoke this condition.
if (cm != null && cm.AcceptsExternalCondition(a, Condition, Duration > 0))
cm.GrantCondition(a, Condition, true, Duration);
if (external != null)
external.GrantCondition(a, firedBy, Duration);
}
}
}

View File

@@ -123,8 +123,8 @@
CloakSound: trans1.aud
UncloakSound: trans1.aud
RequiresCondition: cloak
ExternalConditions@CLOAK:
Conditions: cloak
ExternalCondition@CLOAK:
Condition: cloak
^Vehicle:
Inherits@1: ^ExistsInWorld

View File

@@ -34,20 +34,20 @@ HACKE6:
CaptureTypes: building
Targetable:
RequiresCondition: !jail
ExternalConditions@JAIL:
Conditions: jail
Targetable@PRISONER:
TargetTypes: Prisoner
RenderSprites:
Image: E6
ExternalCondition@JAIL:
Condition: jail
MEDI:
Targetable:
RequiresCondition: !jail
ExternalConditions@JAIL:
Conditions: jail
Targetable@PRISONER:
TargetTypes: Prisoner
ExternalCondition@JAIL:
Condition: jail
PRISON:
HiddenUnderShroud:

View File

@@ -35,20 +35,20 @@ HACKE6:
WithInfantryBody:
Targetable:
RequiresCondition: !jail
ExternalConditions@JAIL:
Conditions: jail
Targetable@PRISONER:
TargetTypes: Prisoner
RenderSprites:
Image: E6
ExternalCondition@JAIL:
Condition: jail
MEDI:
Targetable:
RequiresCondition: !jail
ExternalConditions@JAIL:
Conditions: jail
Targetable@PRISONER:
TargetTypes: Prisoner
ExternalCondition@JAIL:
Condition: jail
PRISON:
HiddenUnderShroud:

View File

@@ -22,8 +22,8 @@ World:
DamageMultiplier@UNKILLABLE:
RequiresCondition: unkillable
Modifier: 0
ExternalConditions:
Conditions: unkillable
ExternalCondition@UNKILLABLE:
Condition: unkillable
^Tank:
GivesBounty:
@@ -33,8 +33,8 @@ World:
DamageMultiplier@UNKILLABLE:
RequiresCondition: unkillable
Modifier: 0
ExternalConditions:
Conditions: unkillable
ExternalCondition@UNKILLABLE:
Condition: unkillable
^Infantry:
GivesBounty:
@@ -50,8 +50,8 @@ World:
DamageMultiplier@UNKILLABLE:
RequiresCondition: unkillable
Modifier: 0
ExternalConditions:
Conditions: unkillable
ExternalCondition@UNKILLABLE:
Condition: unkillable
^Ship:
GivesBounty:
@@ -61,8 +61,8 @@ World:
DamageMultiplier@UNKILLABLE:
RequiresCondition: unkillable
Modifier: 0
ExternalConditions:
Conditions: unkillable
ExternalCondition@UNKILLABLE:
Condition: unkillable
^Plane:
GivesBounty:
@@ -70,8 +70,8 @@ World:
DamageMultiplier@UNKILLABLE:
RequiresCondition: unkillable
Modifier: 0
ExternalConditions:
Conditions: unkillable
ExternalCondition@UNKILLABLE:
Condition: unkillable
^Building:
GivesBounty:
@@ -79,8 +79,8 @@ World:
DamageMultiplier@UNKILLABLE:
RequiresCondition: unkillable
Modifier: 0
ExternalConditions:
Conditions: unkillable
ExternalCondition@UNKILLABLE:
Condition: unkillable
OILB:
CashTrickler:

View File

@@ -59,8 +59,14 @@ V05:
DOG:
# HACK: Disable experience without killing the linter
-GainsExperience:
ExternalConditions@EXPERIENCE:
Conditions: rank-veteran-1, rank-veteran-2, rank-veteran-3, rank-elite
ExternalCondition@RANK-VETERAN-1:
Condition: rank-veteran-1
ExternalCondition@RANK-VETERAN-2:
Condition: rank-veteran-2
ExternalCondition@RANK-VETERAN-3:
Condition: rank-veteran-3
ExternalCondition@RANK-ELITE:
Condition: rank-elite
SPY:
Mobile:

View File

@@ -123,8 +123,8 @@
Modifier: 0
TimedConditionBar:
Condition: invulnerability
ExternalConditions@INVULNERABILITY:
Conditions: invulnerability
ExternalCondition@INVULNERABILITY:
Condition: invulnerability
^Vehicle:
Inherits@1: ^ExistsInWorld

View File

@@ -539,8 +539,6 @@ DOME:
Bib:
ProvidesRadar:
RequiresCondition: !jammed && !disabled
ExternalConditions@JAMMED:
Conditions: jammed
InfiltrateForExploration:
DetectCloaked:
Range: 10c0
@@ -550,6 +548,8 @@ DOME:
ProvidesPrerequisite@buildingname:
GrantConditionOnDisabled@IDISABLE:
Condition: disabled
ExternalCondition@JAMMED:
Condition: jammed
PBOX:
Inherits: ^Defense

View File

@@ -1448,17 +1448,17 @@ Rules:
DamageMultiplier@UNKILLABLE:
RequiresCondition: unkillable
Modifier: 0
ExternalConditions:
Conditions: unkillable
ExternalCondition@UNKILLABLE:
Condition: unkillable
NAOBEL:
DamageMultiplier@UNKILLABLE:
RequiresCondition: unkillable
Modifier: 0
ExternalConditions:
Conditions: unkillable
ExternalCondition@UNKILLABLE:
Condition: unkillable
NALASR:
DamageMultiplier@UNKILLABLE:
RequiresCondition: unkillable
Modifier: 0
ExternalConditions:
Conditions: unkillable
ExternalCondition@UNKILLABLE:
Condition: unkillable

View File

@@ -1296,7 +1296,8 @@ GALITE:
SelectionDecorations:
VisualBounds: 25, 35, 0, -12
-Cloak@EXTERNALCLOAK:
-ExternalConditions@EXTERNALCLOAK:
-ExternalCondition@CLOAKGENERATOR:
-ExternalCondition@CRATE-CLOAK:
TSTLAMP:
Inherits: GALITE

View File

@@ -75,8 +75,12 @@
SpeedMultiplier@CRATES:
RequiresCondition: crate-speed
Modifier: 170
ExternalConditions@CRATES:
Conditions: crate-firepower, crate-damage, crate-speed
ExternalCondition@CRATE-FIREPOWER:
Condition: crate-firepower
ExternalCondition@CRATE-DAMAGE:
Condition: crate-damage
ExternalCondition@CRATE-SPEED:
Condition: crate-speed
^EmpDisable:
WithColoredOverlay@EMPDISABLE:
@@ -96,8 +100,8 @@
PowerMultiplier@EMPDISABLE:
RequiresCondition: empdisable
Modifier: 0
ExternalConditions@EMPDISABLE:
Conditions: empdisable
ExternalCondition@EMPDISABLE:
Condition: empdisable
^Cloakable:
Cloak@EXTERNALCLOAK:
@@ -108,8 +112,10 @@
CloakSound: cloak5.aud
UncloakSound: cloak5.aud
UncloakOn: Attack, Unload, Infiltrate, Demolish, Damage
ExternalConditions@EXTERNALCLOAK:
Conditions: cloakgenerator, crate-cloak
ExternalCondition@CLOAKGENERATOR:
Condition: cloakgenerator
ExternalCondition@CRATE-CLOAK:
Condition: crate-cloak
^BasicBuilding:
Inherits@1: ^ExistsInWorld
@@ -206,7 +212,8 @@
RenderSprites:
Palette: player
-Cloak@EXTERNALCLOAK:
-ExternalConditions@EXTERNALCLOAK:
-ExternalCondition@CLOAKGENERATOR:
-ExternalCondition@CRATE-CLOAK:
^CivBillboard:
Inherits: ^CivBuilding