#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.ComponentModel; using System.Drawing; using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Text.RegularExpressions; using OpenRA.Graphics; using OpenRA.Primitives; using OpenRA.Support; namespace OpenRA { public static class FieldLoader { [Serializable] 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 override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue("Missing", Missing); info.AddValue("Header", Header); } } public static Func InvalidValueAction = (s, t, f) => { throw new YamlException("FieldLoader: Cannot parse `{0}` into `{1}.{2}` ".F(s, f, t)); }; public static Action UnknownFieldAction = (s, f) => { throw new NotImplementedException("FieldLoader: Missing field `{0}` on `{1}`".F(s, f.Name)); }; static readonly ConcurrentCache TypeLoadInfo = new ConcurrentCache(BuildTypeLoadInfo); static readonly ConcurrentCache MemberHasTranslateAttribute = new ConcurrentCache(member => member.HasAttribute()); static readonly object TranslationsLock = new object(); static Dictionary translations; public static void Load(object self, MiniYaml my) { var loadInfo = TypeLoadInfo[self.GetType()]; var missing = new List(); Dictionary md = null; foreach (var fli in loadInfo) { object val; if (md == null) md = my.ToDictionary(); if (fli.Loader != null) { if (!fli.Attribute.Required || md.ContainsKey(fli.YamlName)) val = fli.Loader(my); else { missing.Add(fli.YamlName); continue; } } else { 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) { ret = null; MiniYaml yaml; if (!md.TryGetValue(yamlName, out yaml)) return false; ret = GetValue(field.Name, field.FieldType, yaml, field); return true; } public static T Load(MiniYaml y) where T : new() { var t = new T(); Load(t, y); return t; } public static void LoadField(object target, string key, string value) { const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; key = key.Trim(); var field = target.GetType().GetField(key, Flags); if (field != null) { var sa = field.GetCustomAttributes(false).DefaultIfEmpty(SerializeAttribute.Default).First(); if (!sa.FromYamlKey) field.SetValue(target, GetValue(field.Name, field.FieldType, value, field)); return; } var prop = target.GetType().GetProperty(key, Flags); if (prop != null) { var sa = prop.GetCustomAttributes(false).DefaultIfEmpty(SerializeAttribute.Default).First(); if (!sa.FromYamlKey) prop.SetValue(target, GetValue(prop.Name, prop.PropertyType, value, prop), null); return; } UnknownFieldAction(key, target.GetType()); } public static T GetValue(string field, string value) { return (T)GetValue(field, typeof(T), value, null); } public static object GetValue(string fieldName, Type fieldType, string value) { return GetValue(fieldName, fieldType, value, null); } public static object GetValue(string fieldName, Type fieldType, string value, MemberInfo field) { return GetValue(fieldName, fieldType, new MiniYaml(value), field); } public static object GetValue(string fieldName, Type fieldType, MiniYaml yaml, MemberInfo field) { var value = yaml.Value; if (value != null) value = value.Trim(); if (fieldType == typeof(int)) { int res; if (Exts.TryParseIntegerInvariant(value, out res)) return res; return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(ushort)) { ushort res; if (ushort.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out res)) return res; return InvalidValueAction(value, fieldType, fieldName); } if (fieldType == typeof(long)) { long res; if (long.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out res)) return res; return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(float)) { float res; if (value != null && float.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out res)) return res * (value.Contains('%') ? 0.01f : 1f); return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(decimal)) { decimal res; if (value != null && decimal.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out res)) return res * (value.Contains('%') ? 0.01m : 1m); return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(string)) { if (field != null && MemberHasTranslateAttribute[field] && value != null) return Regex.Replace(value, "@[^@]+@", m => Translate(m.Value.Substring(1, m.Value.Length - 2)), RegexOptions.Compiled); return value; } else if (fieldType == typeof(Color)) { Color color; if (value != null && HSLColor.TryParseRGB(value, out color)) return color; return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(Color[])) { if (value != null) { var parts = value.Split(','); var colors = new Color[parts.Length]; for (var i = 0; i < colors.Length; i++) if (!HSLColor.TryParseRGB(parts[i], out colors[i])) return InvalidValueAction(value, fieldType, fieldName); return colors; } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(HSLColor)) { if (value != null) { Color rgb; if (HSLColor.TryParseRGB(value, out rgb)) return new HSLColor(rgb); // Allow old HSLColor/ColorRamp formats to be parsed as HSLColor var parts = value.Split(','); if (parts.Length == 3 || parts.Length == 4) return new HSLColor( (byte)Exts.ParseIntegerInvariant(parts[0]).Clamp(0, 255), (byte)Exts.ParseIntegerInvariant(parts[1]).Clamp(0, 255), (byte)Exts.ParseIntegerInvariant(parts[2]).Clamp(0, 255)); } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(Hotkey)) { Hotkey res; if (Hotkey.TryParse(value, out res)) return res; return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(WDist)) { WDist res; if (WDist.TryParse(value, out res)) return res; return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(WVec)) { if (value != null) { var parts = value.Split(','); if (parts.Length == 3) { WDist rx, ry, rz; if (WDist.TryParse(parts[0], out rx) && WDist.TryParse(parts[1], out ry) && WDist.TryParse(parts[2], out rz)) return new WVec(rx, ry, rz); } } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(WVec[])) { if (value != null) { var parts = value.Split(','); if (parts.Length % 3 != 0) return InvalidValueAction(value, fieldType, fieldName); var vecs = new WVec[parts.Length / 3]; for (var i = 0; i < vecs.Length; ++i) { WDist rx, ry, rz; if (WDist.TryParse(parts[3 * i], out rx) && WDist.TryParse(parts[3 * i + 1], out ry) && WDist.TryParse(parts[3 * i + 2], out rz)) vecs[i] = new WVec(rx, ry, rz); } return vecs; } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(WPos)) { if (value != null) { var parts = value.Split(','); if (parts.Length == 3) { WDist rx, ry, rz; if (WDist.TryParse(parts[0], out rx) && WDist.TryParse(parts[1], out ry) && WDist.TryParse(parts[2], out rz)) return new WPos(rx, ry, rz); } } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(WAngle)) { int res; if (Exts.TryParseIntegerInvariant(value, out res)) return new WAngle(res); return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(WRot)) { if (value != null) { var parts = value.Split(','); if (parts.Length == 3) { int rr, rp, ry; if (Exts.TryParseIntegerInvariant(parts[0], out rr) && Exts.TryParseIntegerInvariant(parts[1], out rp) && Exts.TryParseIntegerInvariant(parts[2], out ry)) return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry)); } } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(CPos)) { if (value != null) { var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); return new CPos(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1])); } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(CVec)) { if (value != null) { var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); return new CVec(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1])); } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(CVec[])) { if (value != null) { var parts = value.Split(','); if (parts.Length % 2 != 0) return InvalidValueAction(value, fieldType, fieldName); var vecs = new CVec[parts.Length / 2]; for (var i = 0; i < vecs.Length; i++) { int rx, ry; if (int.TryParse(parts[2 * i], out rx) && int.TryParse(parts[2 * i + 1], out ry)) vecs[i] = new CVec(rx, ry); } return vecs; } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(BooleanExpression)) { if (value != null) { try { return new BooleanExpression(value); } catch (InvalidDataException e) { throw new YamlException(e.Message); } } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(IntegerExpression)) { if (value != null) { try { return new IntegerExpression(value); } catch (InvalidDataException e) { throw new YamlException(e.Message); } } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType.IsEnum) { try { return Enum.Parse(fieldType, value, true); } catch (ArgumentException) { return InvalidValueAction(value, fieldType, fieldName); } } else if (fieldType == typeof(ImageFormat)) { if (value != null) { switch (value.ToLowerInvariant()) { case "bmp": return ImageFormat.Bmp; case "gif": return ImageFormat.Gif; case "jpg": case "jpeg": return ImageFormat.Jpeg; case "tif": case "tiff": return ImageFormat.Tiff; default: return ImageFormat.Png; } } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(bool)) return ParseYesNo(value, fieldType, fieldName); else if (fieldType.IsArray && fieldType.GetArrayRank() == 1) { if (value == null) return Array.CreateInstance(fieldType.GetElementType(), 0); var options = field != null && field.HasAttribute() ? StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries; var parts = value.Split(new char[] { ',' }, options); var ret = Array.CreateInstance(fieldType.GetElementType(), parts.Length); for (var i = 0; i < parts.Length; i++) ret.SetValue(GetValue(fieldName, fieldType.GetElementType(), parts[i].Trim(), field), i); return ret; } else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(HashSet<>)) { var set = Activator.CreateInstance(fieldType); if (value == null) return set; var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var addMethod = fieldType.GetMethod("Add", fieldType.GetGenericArguments()); for (var i = 0; i < parts.Length; i++) addMethod.Invoke(set, new[] { GetValue(fieldName, fieldType.GetGenericArguments()[0], parts[i].Trim(), field) }); return set; } else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { var dict = Activator.CreateInstance(fieldType); var arguments = fieldType.GetGenericArguments(); var addMethod = fieldType.GetMethod("Add", arguments); foreach (var node in yaml.Nodes) { var key = GetValue(fieldName, arguments[0], node.Key, field); var val = GetValue(fieldName, arguments[1], node.Value, field); addMethod.Invoke(dict, new[] { key, val }); } return dict; } else if (fieldType == typeof(Size)) { if (value != null) { var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); return new Size(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1])); } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(int2)) { if (value != null) { var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); return new int2(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1])); } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(float2)) { if (value != null) { var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); float xx = 0; float yy = 0; float res; if (float.TryParse(parts[0].Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out res)) xx = res * (parts[0].Contains('%') ? 0.01f : 1f); if (float.TryParse(parts[1].Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out res)) yy = res * (parts[1].Contains('%') ? 0.01f : 1f); return new float2(xx, yy); } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(float3)) { if (value != null) { var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); float x = 0; float y = 0; float z = 0; float.TryParse(parts[0], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out x); float.TryParse(parts[1], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out y); // z component is optional for compatibility with older float2 definitions if (parts.Length > 2) float.TryParse(parts[2], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out z); return new float3(x, y, z); } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(Rectangle)) { if (value != null) { var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); return new Rectangle( Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]), Exts.ParseIntegerInvariant(parts[2]), Exts.ParseIntegerInvariant(parts[3])); } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Bits<>)) { if (value != null) { var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var argTypes = new Type[] { typeof(string[]) }; var argValues = new object[] { parts }; return fieldType.GetConstructor(argTypes).Invoke(argValues); } return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Nullable<>)) { var innerType = fieldType.GetGenericArguments().First(); var innerValue = GetValue("Nullable", innerType, value, field); return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue }); } else if (fieldType == typeof(DateTime)) { DateTime dt; if (DateTime.TryParseExact(value, "yyyy-MM-dd HH-mm-ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out dt)) return dt; return InvalidValueAction(value, fieldType, fieldName); } else { var conv = TypeDescriptor.GetConverter(fieldType); if (conv.CanConvertFrom(typeof(string))) { try { return conv.ConvertFromInvariantString(value); } catch { return InvalidValueAction(value, fieldType, fieldName); } } } UnknownFieldAction("[Type] {0}".F(value), fieldType); return null; } static object ParseYesNo(string p, Type fieldType, string field) { if (string.IsNullOrEmpty(p)) return InvalidValueAction(p, fieldType, field); p = p.ToLowerInvariant(); if (p == "yes") return true; if (p == "true") return true; if (p == "no") return false; if (p == "false") return false; return InvalidValueAction(p, fieldType, field); } public sealed class FieldLoadInfo { public readonly FieldInfo Field; public readonly SerializeAttribute Attribute; public readonly string YamlName; public readonly Func Loader; internal FieldLoadInfo(FieldInfo field, SerializeAttribute attr, string yamlName, Func loader = null) { Field = field; Attribute = attr; YamlName = yamlName; Loader = loader; } } public static IEnumerable GetTypeLoadInfo(Type type, bool includePrivateByDefault = false) { return TypeLoadInfo[type].Where(fli => includePrivateByDefault || fli.Field.IsPublic || (fli.Attribute.Serialize && !fli.Attribute.IsDefault)); } static FieldLoadInfo[] BuildTypeLoadInfo(Type type) { var ret = new List(); foreach (var ff in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { var field = ff; var sa = field.GetCustomAttributes(false).DefaultIfEmpty(SerializeAttribute.Default).First(); if (!sa.Serialize) continue; var yamlName = string.IsNullOrEmpty(sa.YamlName) ? field.Name : sa.YamlName; var loader = sa.GetLoader(type); if (loader == null && sa.FromYamlKey) loader = yaml => GetValue(yamlName, field.FieldType, yaml, field); var fli = new FieldLoadInfo(field, sa, yamlName, loader); ret.Add(fli); } return ret.ToArray(); } [AttributeUsage(AttributeTargets.Field)] public sealed class IgnoreAttribute : SerializeAttribute { public IgnoreAttribute() : base(false) { } } [AttributeUsage(AttributeTargets.Field)] public sealed class RequireAttribute : SerializeAttribute { public RequireAttribute() : base(true, true) { } } [AttributeUsage(AttributeTargets.Field)] public sealed class AllowEmptyEntriesAttribute : SerializeAttribute { public AllowEmptyEntriesAttribute() : base(allowEmptyEntries: true) { } } [AttributeUsage(AttributeTargets.Field)] public sealed class LoadUsingAttribute : SerializeAttribute { public LoadUsingAttribute(string loader, bool required = false) { Loader = loader; Required = required; } } [AttributeUsage(AttributeTargets.Field)] public class SerializeAttribute : Attribute { public static readonly SerializeAttribute Default = new SerializeAttribute(true); public bool IsDefault { get { return this == Default; } } public readonly bool Serialize; public string YamlName; public string Loader; public bool FromYamlKey; public bool DictionaryFromYamlKey; public bool Required; public bool AllowEmptyEntries; public SerializeAttribute(bool serialize = true, bool required = false, bool allowEmptyEntries = false) { Serialize = serialize; Required = required; AllowEmptyEntries = allowEmptyEntries; } internal Func GetLoader(Type type) { const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.FlattenHierarchy; if (!string.IsNullOrEmpty(Loader)) { var method = type.GetMethod(Loader, Flags); if (method == null) throw new InvalidOperationException("{0} does not specify a loader function '{1}'".F(type.Name, Loader)); return (Func)Delegate.CreateDelegate(typeof(Func), method); } return null; } } public static string Translate(string key) { if (string.IsNullOrEmpty(key)) return key; lock (TranslationsLock) { if (translations == null) return key; string value; if (!translations.TryGetValue(key, out value)) return key; return value; } } public static void SetTranslations(IDictionary translations) { lock (TranslationsLock) FieldLoader.translations = new Dictionary(translations); } } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public sealed class TranslateAttribute : Attribute { } [AttributeUsage(AttributeTargets.Field)] public sealed class FieldFromYamlKeyAttribute : FieldLoader.SerializeAttribute { public FieldFromYamlKeyAttribute() { FromYamlKey = true; } } // Special-cases FieldFromYamlKeyAttribute for use with Dictionary. [AttributeUsage(AttributeTargets.Field)] public sealed class DictionaryFromYamlKeyAttribute : FieldLoader.SerializeAttribute { public DictionaryFromYamlKeyAttribute() { FromYamlKey = true; DictionaryFromYamlKey = true; } } // Mirrors DescriptionAttribute from System.ComponentModel but we don't want to have to use that everywhere. [AttributeUsage(AttributeTargets.All)] public sealed class DescAttribute : Attribute { public readonly string[] Lines; public DescAttribute(params string[] lines) { Lines = lines; } } }