diff --git a/OpenRA.Game/Traits/LintAttributes.cs b/OpenRA.Game/Traits/LintAttributes.cs index b339b5fae5..c34690f64a 100644 --- a/OpenRA.Game/Traits/LintAttributes.cs +++ b/OpenRA.Game/Traits/LintAttributes.cs @@ -15,7 +15,14 @@ namespace OpenRA.Traits /* attributes used by OpenRA.Lint to understand the rules */ [AttributeUsage(AttributeTargets.Field)] - public sealed class ActorReferenceAttribute : Attribute { } + public sealed class ActorReferenceAttribute : Attribute + { + public Type[] RequiredTraits; + public ActorReferenceAttribute(params Type[] requiredTraits) + { + RequiredTraits = requiredTraits; + } + } [AttributeUsage(AttributeTargets.Field)] public sealed class WeaponReferenceAttribute : Attribute { } diff --git a/OpenRA.Mods.Common/Lint/CheckActorReferences.cs b/OpenRA.Mods.Common/Lint/CheckActorReferences.cs index f71d58ee48..55e7f14cc7 100644 --- a/OpenRA.Mods.Common/Lint/CheckActorReferences.cs +++ b/OpenRA.Mods.Common/Lint/CheckActorReferences.cs @@ -11,6 +11,7 @@ using System; using System.Linq; using System.Reflection; +using OpenRA.GameRules; using OpenRA.Traits; namespace OpenRA.Mods.Common.Lint @@ -34,22 +35,84 @@ namespace OpenRA.Mods.Common.Lint foreach (var field in actualType.GetFields()) { if (field.HasAttribute()) - CheckReference(actorInfo, traitInfo, field, rules.Actors, "actor"); + CheckActorReference(actorInfo, traitInfo, field, rules.Actors, + field.GetCustomAttributes(true)[0]); + if (field.HasAttribute()) - CheckReference(actorInfo, traitInfo, field, rules.Weapons, "weapon"); + CheckWeaponReference(actorInfo, traitInfo, field, rules.Weapons, + field.GetCustomAttributes(true)[0]); + if (field.HasAttribute()) - CheckReference(actorInfo, traitInfo, field, rules.Voices, "voice"); + CheckVoiceReference(actorInfo, traitInfo, field, rules.Voices, + field.GetCustomAttributes(true)[0]); } } - void CheckReference(ActorInfo actorInfo, ITraitInfo traitInfo, FieldInfo fieldInfo, - IReadOnlyDictionary dict, string type) + void CheckActorReference(ActorInfo actorInfo, + ITraitInfo traitInfo, + FieldInfo fieldInfo, + IReadOnlyDictionary dict, + ActorReferenceAttribute attribute) { var values = LintExts.GetFieldValues(traitInfo, fieldInfo, emitError); - foreach (var v in values) - if (v != null && !dict.ContainsKey(v.ToLowerInvariant())) - emitError("{0}.{1}.{2}: Missing {3} `{4}`." - .F(actorInfo.Name, traitInfo.GetType().Name, fieldInfo.Name, type, v)); + foreach (var value in values) + { + if (value == null) + continue; + + // NOTE: Once https://github.com/OpenRA/OpenRA/issues/4124 is resolved we won't + // have to .ToLower* anything here. + var v = value.ToLowerInvariant(); + + if (!dict.ContainsKey(v)) + { + emitError("{0}.{1}.{2}: Missing actor `{3}`." + .F(actorInfo.Name, traitInfo.GetType().Name, fieldInfo.Name, value)); + + continue; + } + + foreach (var requiredTrait in attribute.RequiredTraits) + if (!dict[v].TraitsInConstructOrder().Any(t => t.GetType() == requiredTrait || t.GetType().IsSubclassOf(requiredTrait))) + emitError("Actor type {0} does not have trait {1} which is required by {2}.{3}." + .F(value, requiredTrait.Name, traitInfo.GetType().Name, fieldInfo.Name)); + } + } + + void CheckWeaponReference(ActorInfo actorInfo, + ITraitInfo traitInfo, + FieldInfo fieldInfo, + IReadOnlyDictionary dict, + WeaponReferenceAttribute attribute) + { + var values = LintExts.GetFieldValues(traitInfo, fieldInfo, emitError); + foreach (var value in values) + { + if (value == null) + continue; + + if (!dict.ContainsKey(value.ToLower())) + emitError("{0}.{1}.{2}: Missing weapon `{3}`." + .F(actorInfo.Name, traitInfo.GetType().Name, fieldInfo.Name, value)); + } + } + + void CheckVoiceReference(ActorInfo actorInfo, + ITraitInfo traitInfo, + FieldInfo fieldInfo, + IReadOnlyDictionary dict, + VoiceSetReferenceAttribute attribute) + { + var values = LintExts.GetFieldValues(traitInfo, fieldInfo, emitError); + foreach (var value in values) + { + if (value == null) + continue; + + if (!dict.ContainsKey(value.ToLower())) + emitError("{0}.{1}.{2}: Missing voice `{3}`." + .F(actorInfo.Name, traitInfo.GetType().Name, fieldInfo.Name, value)); + } } } -} \ No newline at end of file +} diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs index 460628c582..3876f7bade 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs @@ -21,7 +21,7 @@ namespace OpenRA.Mods.Common.Traits { public class AirstrikePowerInfo : SupportPowerInfo { - [ActorReference] + [ActorReference(typeof(AircraftInfo))] public readonly string UnitType = "badr.bomber"; public readonly int SquadSize = 1; public readonly WVec SquadOffset = new WVec(-1536, 1536, 0); diff --git a/OpenRA.Mods.RA/Traits/SupportPowers/ParatroopersPower.cs b/OpenRA.Mods.RA/Traits/SupportPowers/ParatroopersPower.cs index b4e1933790..9eec3c8d3f 100644 --- a/OpenRA.Mods.RA/Traits/SupportPowers/ParatroopersPower.cs +++ b/OpenRA.Mods.RA/Traits/SupportPowers/ParatroopersPower.cs @@ -21,7 +21,7 @@ namespace OpenRA.Mods.RA.Traits { public class ParatroopersPowerInfo : SupportPowerInfo { - [ActorReference] + [ActorReference(typeof(AircraftInfo))] public readonly string UnitType = "badr"; public readonly int SquadSize = 1; public readonly WVec SquadOffset = new WVec(-1536, 1536, 0); @@ -32,7 +32,7 @@ namespace OpenRA.Mods.RA.Traits [Desc("Spawn and remove the plane this far outside the map.")] public readonly WDist Cordon = new WDist(5120); - [ActorReference] + [ActorReference(typeof(PassengerInfo))] [Desc("Troops to be delivered. They will be distributed between the planes if SquadSize > 1.")] public readonly string[] DropItems = { };