diff --git a/OpenRA.Game/Traits/LintAttributes.cs b/OpenRA.Game/Traits/LintAttributes.cs index 70dfa4cfe5..2f3776921c 100644 --- a/OpenRA.Game/Traits/LintAttributes.cs +++ b/OpenRA.Game/Traits/LintAttributes.cs @@ -13,6 +13,13 @@ using System; namespace OpenRA.Traits { + public enum LintDictionaryReference + { + None = 0, + Keys = 1, + Values = 2 + } + [AttributeUsage(AttributeTargets.Field)] public sealed class ActorReferenceAttribute : Attribute { @@ -29,15 +36,19 @@ namespace OpenRA.Traits [AttributeUsage(AttributeTargets.Field)] public sealed class SequenceReferenceAttribute : Attribute { - public readonly string ImageReference; // The field name in the same trait info that contains the image name. + // The field name in the same trait info that contains the image name. + public readonly string ImageReference; public readonly bool Prefix; public readonly bool AllowNullImage; + public readonly LintDictionaryReference DictionaryReference; - public SequenceReferenceAttribute(string imageReference = null, bool prefix = false, bool allowNullImage = false) + public SequenceReferenceAttribute(string imageReference = null, bool prefix = false, bool allowNullImage = false, + LintDictionaryReference dictionaryReference = LintDictionaryReference.None) { ImageReference = imageReference; Prefix = prefix; AllowNullImage = allowNullImage; + DictionaryReference = dictionaryReference; } } diff --git a/OpenRA.Mods.Common/Lint/CheckSequences.cs b/OpenRA.Mods.Common/Lint/CheckSequences.cs index 705d53f701..86acb9c444 100644 --- a/OpenRA.Mods.Common/Lint/CheckSequences.cs +++ b/OpenRA.Mods.Common/Lint/CheckSequences.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using OpenRA.Graphics; using OpenRA.Mods.Common.Traits.Render; using OpenRA.Traits; @@ -83,7 +84,7 @@ namespace OpenRA.Mods.Common.Lint sequenceImages = new[] { imageOverride.ToLowerInvariant() }; } - foreach (var sequence in LintExts.GetFieldValues(traitInfo, field, emitError)) + foreach (var sequence in LintExts.GetFieldValues(traitInfo, field, emitError, sequenceReference.DictionaryReference)) { if (string.IsNullOrEmpty(sequence)) continue; @@ -128,7 +129,7 @@ namespace OpenRA.Mods.Common.Lint } image = image.ToLowerInvariant(); - foreach (var sequence in LintExts.GetFieldValues(projectileInfo, field, emitError)) + foreach (var sequence in LintExts.GetFieldValues(projectileInfo, field, emitError, sequenceReference.DictionaryReference)) { if (string.IsNullOrEmpty(sequence)) continue; diff --git a/OpenRA.Mods.Common/Lint/LintExts.cs b/OpenRA.Mods.Common/Lint/LintExts.cs index b2e4224cb1..3257c31ed1 100644 --- a/OpenRA.Mods.Common/Lint/LintExts.cs +++ b/OpenRA.Mods.Common/Lint/LintExts.cs @@ -15,12 +15,14 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using OpenRA.Support; +using OpenRA.Traits; namespace OpenRA.Mods.Common.Lint { public class LintExts { - public static IEnumerable GetFieldValues(object ruleInfo, FieldInfo fieldInfo, Action emitError) + public static IEnumerable GetFieldValues(object ruleInfo, FieldInfo fieldInfo, Action emitError, + LintDictionaryReference dictionaryReference = LintDictionaryReference.None) { var type = fieldInfo.FieldType; if (type == typeof(string)) @@ -35,11 +37,38 @@ namespace OpenRA.Mods.Common.Lint return expr != null ? expr.Variables : Enumerable.Empty(); } - throw new InvalidOperationException("Bad type for reference on {0}.{1}. Supported types: string, IEnumerable, BooleanExpression, IntegerExpression" - .F(ruleInfo.GetType().Name, fieldInfo.Name)); + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + // Use an intermediate list to cover the unlikely case where both keys and values are lintable + var dictionaryValues = new List(); + if (dictionaryReference.HasFlag(LintDictionaryReference.Keys) && type.GenericTypeArguments[0] == typeof(string)) + dictionaryValues.AddRange((IEnumerable)((IDictionary)fieldInfo.GetValue(ruleInfo)).Keys); + + if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && type.GenericTypeArguments[1] == typeof(string)) + dictionaryValues.AddRange((IEnumerable)((IDictionary)fieldInfo.GetValue(ruleInfo)).Values); + + if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && type.GenericTypeArguments[1] == typeof(IEnumerable)) + foreach (var row in (IEnumerable>)((IDictionary)fieldInfo.GetValue(ruleInfo)).Values) + dictionaryValues.AddRange(row); + + return dictionaryValues; + } + + var supportedTypes = new[] + { + "string", "IEnumerable", + "Dictionary (LintDictionaryReference.Keys)", + "Dictionary (LintDictionaryReference.Values)", + "Dictionary> (LintDictionaryReference.Values)", + "BooleanExpression", "IntegerExpression" + }; + + throw new InvalidOperationException("Bad type for reference on {0}.{1}. Supported types: {2}" + .F(ruleInfo.GetType().Name, fieldInfo.Name, supportedTypes.JoinWith(", "))); } - public static IEnumerable GetPropertyValues(object ruleInfo, PropertyInfo propertyInfo, Action emitError) + public static IEnumerable GetPropertyValues(object ruleInfo, PropertyInfo propertyInfo, Action emitError, + LintDictionaryReference dictionaryReference = LintDictionaryReference.None) { var type = propertyInfo.PropertyType; if (type == typeof(string)) @@ -54,8 +83,34 @@ namespace OpenRA.Mods.Common.Lint return expr != null ? expr.Variables : Enumerable.Empty(); } - throw new InvalidOperationException("Bad type for reference on {0}.{1}. Supported types: string, IEnumerable, BooleanExpression, IntegerExpression" - .F(ruleInfo.GetType().Name, propertyInfo.Name)); + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + // Use an intermediate list to cover the unlikely case where both keys and values are lintable + var dictionaryValues = new List(); + if (dictionaryReference.HasFlag(LintDictionaryReference.Keys) && type.GenericTypeArguments[0] == typeof(string)) + dictionaryValues.AddRange((IEnumerable)((IDictionary)propertyInfo.GetValue(ruleInfo)).Keys); + + if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && type.GenericTypeArguments[1] == typeof(string)) + dictionaryValues.AddRange((IEnumerable)((IDictionary)propertyInfo.GetValue(ruleInfo)).Values); + + if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && type.GenericTypeArguments[1] == typeof(IEnumerable)) + foreach (var row in (IEnumerable>)((IDictionary)propertyInfo.GetValue(ruleInfo)).Values) + dictionaryValues.AddRange(row); + + return dictionaryValues; + } + + var supportedTypes = new[] + { + "string", "IEnumerable", + "Dictionary (LintDictionaryReference.Keys)", + "Dictionary (LintDictionaryReference.Values)", + "Dictionary> (LintDictionaryReference.Values)", + "BooleanExpression", "IntegerExpression" + }; + + throw new InvalidOperationException("Bad type for reference on {0}.{1}. Supported types: {2}" + .F(ruleInfo.GetType().Name, propertyInfo.Name, supportedTypes.JoinWith(", "))); } } } diff --git a/OpenRA.Mods.Common/Traits/Render/WithCargoPipsDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithCargoPipsDecoration.cs index bd10e022bf..a7e95e4126 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithCargoPipsDecoration.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithCargoPipsDecoration.cs @@ -34,7 +34,7 @@ namespace OpenRA.Mods.Common.Traits.Render [Desc("Sequence used for full pips that aren't defined in CustomPipSequences.")] public readonly string FullSequence = "pip-green"; - // TODO: [SequenceReference] isn't smart enough to use Dictionaries. + [SequenceReference("Image", dictionaryReference: LintDictionaryReference.Values)] [Desc("Pip sequence to use for specific passenger actors.")] public readonly Dictionary CustomPipSequences = new Dictionary(); diff --git a/OpenRA.Mods.Common/Traits/Render/WithHarvesterPipsDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithHarvesterPipsDecoration.cs index 488e8f0479..6f707d323a 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithHarvesterPipsDecoration.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithHarvesterPipsDecoration.cs @@ -35,7 +35,7 @@ namespace OpenRA.Mods.Common.Traits.Render [Desc("Sequence used for full pips that aren't defined in ResourceSequences.")] public readonly string FullSequence = "pip-green"; - // TODO: [SequenceReference] isn't smart enough to use Dictionaries. + [SequenceReference("Image", dictionaryReference: LintDictionaryReference.Values)] [Desc("Pip sequence to use for specific resource types.")] public readonly Dictionary ResourceSequences = new Dictionary(); diff --git a/OpenRA.Mods.Common/Traits/Render/WithInfantryBody.cs b/OpenRA.Mods.Common/Traits/Render/WithInfantryBody.cs index 72864a26e6..1a3f99b3d2 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithInfantryBody.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithInfantryBody.cs @@ -28,7 +28,7 @@ namespace OpenRA.Mods.Common.Traits.Render [SequenceReference] public readonly string DefaultAttackSequence = null; - // TODO: [SequenceReference] isn't smart enough to use Dictionaries. + [SequenceReference(dictionaryReference: LintDictionaryReference.Values)] [Desc("Attack sequence to use for each armament.")] public readonly Dictionary AttackSequences = new Dictionary();