diff --git a/OpenRA.Game/FieldLoader.cs b/OpenRA.Game/FieldLoader.cs index 21806ea98e..52273e288a 100644 --- a/OpenRA.Game/FieldLoader.cs +++ b/OpenRA.Game/FieldLoader.cs @@ -24,6 +24,26 @@ namespace OpenRA { public static class FieldLoader { + public class MissingFieldsException : YamlException + { + public readonly string[] Missing; + public readonly string Header; + public override string Message + { + get + { + return (string.IsNullOrEmpty(Header) ? "" : Header + ": ") + Missing[0] + + string.Concat(Missing.Skip(1).Select(m => ", " + m)); + } + } + + public MissingFieldsException(string[] missing, string header = null, string headerSingle = null) : base(null) + { + Header = missing.Length > 1 ? header : headerSingle ?? header; + Missing = missing; + } + } + public static Func InvalidValueAction = (s, t, f) => { throw new InvalidOperationException("FieldLoader: Cannot parse `{0}` into `{1}.{2}` ".F(s, f, t)); @@ -45,6 +65,7 @@ namespace OpenRA public static void Load(object self, MiniYaml my) { var loadInfo = TypeLoadInfo[self.GetType()]; + var missing = new List(); Dictionary md = null; @@ -52,19 +73,33 @@ namespace OpenRA { object val; + if (md == null) + md = my.ToDictionary(); if (fli.Loader != null) - val = fli.Loader(my); + { + if (!fli.Attribute.Required || md.ContainsKey(fli.YamlName)) + val = fli.Loader(my); + else + { + missing.Add(fli.YamlName); + continue; + } + } else { - if (md == null) - md = my.ToDictionary(); - if (!TryGetValueFromYaml(fli.YamlName, fli.Field, md, out val)) + { + if (fli.Attribute.Required) + missing.Add(fli.YamlName); continue; + } } fli.Field.SetValue(self, val); } + + if (missing.Any()) + throw new MissingFieldsException(missing.ToArray()); } static bool TryGetValueFromYaml(string yamlName, FieldInfo field, Dictionary md, out object ret) @@ -557,12 +592,20 @@ namespace OpenRA : base(false) { } } + [AttributeUsage(AttributeTargets.Field)] + public sealed class RequireAttribute : SerializeAttribute + { + public RequireAttribute() + : base(true, true) { } + } + [AttributeUsage(AttributeTargets.Field)] public sealed class LoadUsingAttribute : SerializeAttribute { - public LoadUsingAttribute(string loader) + public LoadUsingAttribute(string loader, bool required = false) { Loader = loader; + Required = required; } } @@ -577,10 +620,12 @@ namespace OpenRA public string YamlName; public string Loader; public bool FromYamlKey; + public bool Required; - public SerializeAttribute(bool serialize = true) + public SerializeAttribute(bool serialize = true, bool required = false) { Serialize = serialize; + Required = required; } internal Func GetLoader(Type type) diff --git a/OpenRA.Game/GameRules/ActorInfo.cs b/OpenRA.Game/GameRules/ActorInfo.cs index 65d2060c00..ee0ecbb7b4 100644 --- a/OpenRA.Game/GameRules/ActorInfo.cs +++ b/OpenRA.Game/GameRules/ActorInfo.cs @@ -94,7 +94,16 @@ namespace OpenRA throw new YamlException("Junk value `{0}` on trait node {1}" .F(my.Value, traitName)); var info = Game.CreateObject(traitName + "Info"); - FieldLoader.Load(info, my); + try + { + FieldLoader.Load(info, my); + } + catch (FieldLoader.MissingFieldsException e) + { + var header = "Trait name " + traitName + ": " + (e.Missing.Length > 1 ? "Required properties missing" : "Required property missing"); + throw new FieldLoader.MissingFieldsException(e.Missing, header); + } + return info; } diff --git a/OpenRA.Mods.Common/Traits/GainsExperience.cs b/OpenRA.Mods.Common/Traits/GainsExperience.cs index 8644671a4d..c76e5ad808 100644 --- a/OpenRA.Mods.Common/Traits/GainsExperience.cs +++ b/OpenRA.Mods.Common/Traits/GainsExperience.cs @@ -20,8 +20,8 @@ namespace OpenRA.Mods.Common.Traits [Desc("This actor's experience increases when it has killed a GivesExperience actor.")] public class GainsExperienceInfo : ITraitInfo, Requires, Requires { - [FieldLoader.LoadUsing("LoadUpgrades")] - [Desc("Upgrades to grant at each level. (Required property)", + [FieldLoader.LoadUsing("LoadUpgrades", true)] + [Desc("Upgrades to grant at each level.", "Key is the XP requirements for each level as a percentage of our own value.", "Value is a list of the upgrade types to grant")] public readonly Dictionary Upgrades = null; @@ -36,10 +36,7 @@ namespace OpenRA.Mods.Common.Traits static object LoadUpgrades(MiniYaml y) { - MiniYaml upgrades; - - if (!y.ToDictionary().TryGetValue("Upgrades", out upgrades)) - throw new YamlException("GainsExperience is missing Upgrades."); + MiniYaml upgrades = y.ToDictionary()["Upgrades"]; return upgrades.Nodes.ToDictionary( kv => FieldLoader.GetValue("(key)", kv.Key), diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractTraitDocsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractTraitDocsCommand.cs index faf688bcab..df4e2fd00d 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractTraitDocsCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractTraitDocsCommand.cs @@ -86,7 +86,8 @@ namespace OpenRA.Mods.Common.UtilityCommands { var fieldDescLines = info.Field.GetCustomAttributes(true).SelectMany(d => d.Lines); var fieldType = FriendlyTypeName(info.Field.FieldType); - var defaultValue = FieldSaver.SaveField(liveTraitInfo, info.Field.Name).Value.Value; + var loadInfo = info.Field.GetCustomAttributes(true).FirstOrDefault(); + var defaultValue = loadInfo != null && loadInfo.Required ? "(required)" : FieldSaver.SaveField(liveTraitInfo, info.Field.Name).Value.Value; doc.Append("{0}{1}{2}".F(info.YamlName, defaultValue, fieldType)); doc.Append(""); foreach (var line in fieldDescLines)