예제 #1
0
파일: Converter.cs 프로젝트: themotte/utils
        /// <summary>
        /// Handles serialization and deserialization with support for references.
        /// </summary>
        /// <remarks>
        /// In read mode, `model` will be one of the types provided in HandledTypes(); it will not be null. Use recorder or recorder.Xml to serialize its contents, then return model.
        ///
        /// In write mode, `model` will be one of the types provided in HandledTypes(), or null. If it's null, create an object of an appropriate type. Use recorder or recorder.Xml to fill it, then return it.
        ///
        /// `type` indicates the type that the underlying code expects to get back. The object you return must be assignable to that type.
        ///
        /// In case of error, call Def.Dbg.Err with some appropriately useful message, then return model.
        ///
        /// In most cases, using Recorder's interface is by far the easiest way to support the requirements of this function. It is expected that you use XML only when absolutely necessary.
        /// </remarks>
        /// <example>
        /// <code>
        ///     public override object Record(object model, Type type, Recorder recorder)
        ///     {
        ///         // If we're in read mode, this leaves model untouched. If we're in write mode, this leaves model untouched unless it's null.
        ///         model = model ?? new ConvertedClass();
        ///
        ///         // The Recorder interface figures out the right thing based on context.
        ///         // Any members that are referenced elsewhere will be turned into refs automatically.
        ///         recorder.Record(ref model.integerMember, "integerMember");
        ///         recorder.Record(ref model.classMember, "classMember");
        ///         recorder.Record(ref model.structMember, "structMember");
        ///         recorder.Record(ref model.collectionMember, "collectionMember");
        ///
        ///         return model;
        ///     }
        /// </code>
        /// </example>
        public virtual object Record(object model, Type type, Recorder recorder)
        {
            switch (recorder.Mode)
            {
            case Recorder.Direction.Read:
            {
                var sourceName = (recorder as RecorderReader).SourceName;
                var element    = recorder.Xml;

                if (element.Elements().Any())
                {
                    return(FromXml(recorder.Xml, type, sourceName));
                }
                else
                {
                    return(FromString(element.GetText() ?? "", type, sourceName, element.LineNumber()));
                }
            }

            case Recorder.Direction.Write:
                ToXml(model, recorder.Xml);
                return(model);

            default:
                // what have you done
                // *what have you done*
                Dbg.Err($"Recorder is somehow in mode {recorder.Mode} which is not valid");
                return(model);
            }
        }
예제 #2
0
        internal static Type GetDefRootType(this Type type)
        {
            if (!GetDefRootTypeCache.TryGetValue(type, out var result))
            {
                if (GetDefDatabaseStatus(type) <= DefDatabaseStatus.Abstract)
                {
                    Dbg.Err($"{type} does not exist within a database hierarchy.");
                    result = null;
                }
                else
                {
                    Type currentType = type;
                    while (GetDefDatabaseStatus(currentType) == DefDatabaseStatus.Branch)
                    {
                        currentType = currentType.BaseType;
                    }

                    result = currentType;
                }

                GetDefRootTypeCache.Add(type, result);
            }

            return(result);
        }
예제 #3
0
        private static Type GetTypeFromAnyAssembly(string text, string inputLine, int lineNumber)
        {
            // This is technically unnecessary if we're not parsing a generic, but we may as well do it because the cache will still be faster for nongenerics.
            // If we really wanted a perf boost here, we'd do one pass for non-template objects, then do it again on a cache miss to fill it with template stuff.
            // But there's probably much better performance boosts to be seen throughout this.
            if (StrippedTypeCache == null)
            {
                StrippedTypeCache = AppDomain.CurrentDomain.GetAssemblies()
                                    .SelectMany(asm => asm.GetTypes())
                                    .Distinct()
                                    .GroupBy(t => GenericParameterMatcher.Replace(t.FullName, ""))
                                    .ToDictionary(
                    group => group.Key,
                    group => group.ToArray());
            }

            var result = StrippedTypeCache.TryGetValue(text);

            if (result == null)
            {
                return(null);
            }
            else if (result.Length == 1)
            {
                return(result[0]);
            }
            else
            {
                Dbg.Err($"{inputLine}:{lineNumber}: Too many types found with name {text}");
                return(result[0]);
            }
        }
예제 #4
0
        internal static FieldInfo GetFieldFromHierarchy(this Type type, string name)
        {
            FieldInfo result     = null;
            Type      resultType = null;

            Type curType = type;

            while (curType != null)
            {
                FieldInfo typeField = curType.GetField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
                if (typeField != null)
                {
                    if (result == null)
                    {
                        result     = typeField;
                        resultType = curType;
                    }
                    else
                    {
                        Dbg.Err($"Found multiple examples of field named {name} in type hierarchy {type}; found in {resultType} and {curType}");
                    }
                }

                curType = curType.BaseType;
            }
            return(result);
        }
예제 #5
0
파일: Parser.cs 프로젝트: themotte/utils
 internal static void Clear()
 {
     if (s_Status != Status.Finished && s_Status != Status.Uninitialized)
     {
         Dbg.Err($"Clearing while the world is in {s_Status} state; should be {Status.Uninitialized} state or {Status.Finished} state");
     }
     s_Status = Status.Uninitialized;
 }
예제 #6
0
파일: Database.cs 프로젝트: themotte/utils
        /// <summary>
        /// Deletes an existing def.
        /// </summary>
        /// <remarks>
        /// This exists mostly for the Writer functionality. It is generally not recommended to use this during actual gameplay.
        ///
        /// At this time, this will not unregister existing Indexes. This behavior may change at some point in the future.
        /// </remarks>
        public static void Delete(Def def)
        {
            if (Get(def.GetType(), def.DefName) != def)
            {
                Dbg.Err($"Attempting to delete {def} when it either has already been deleted or never existed");
                return;
            }

            Unregister(def);
        }
예제 #7
0
        internal static IndexInfo[] GetIndicesForType(Type type)
        {
            if (IndexInfoCached.TryGetValue(type, out var result))
            {
                // found it in cache, we're done
                return(result);
            }

            IndexInfo[] indices = null;

            if (type.BaseType != null)
            {
                indices = GetIndicesForType(type.BaseType);
            }

            FieldInfo matchedField = null;

            foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
            {
                if (field.GetCustomAttribute <IndexAttribute>() != null)
                {
                    if (matchedField != null)
                    {
                        Dbg.Err($"Too many indices in type {type} (found {matchedField} and {field}); only one will be filled");
                    }

                    matchedField = field;
                }
            }

            if (matchedField != null)
            {
                IndexInfo[] indicesWorking;

                if (indices != null)
                {
                    indicesWorking = new IndexInfo[indices.Length + 1];
                    Array.Copy(indices, indicesWorking, indices.Length);
                }
                else
                {
                    indicesWorking = new IndexInfo[1];
                }

                indicesWorking[indicesWorking.Length - 1] = new IndexInfo {
                    type = type, field = matchedField
                };

                indices = indicesWorking;
            }

            IndexInfoCached[type] = indices;

            return(indices);
        }
예제 #8
0
파일: Database.cs 프로젝트: themotte/utils
        /// <summary>
        /// Creates a Def.
        /// </summary>
        /// <remarks>
        /// This will be supported for dynamically-generated Defs in the future, but right now exists mostly for the Writer functionality. It is currently not recommended to use this during actual gameplay.
        ///
        /// At this time, this will not register Indexes. This behavior may change at some point in the future.
        /// </remarks>
        public static Def Create(Type type, string defName)
        {
            if (!typeof(Def).IsAssignableFrom(type))
            {
                Dbg.Err($"Attempting to dynamically create a Def of type {type}, which is not actually a Def");
                return(null);
            }

            // This is definitely not the most efficient way to do this.
            var createMethod = typeof(Database).GetMethod("Create", new[] { typeof(string) });
            var madeMethod   = createMethod.MakeGenericMethod(new[] { type });

            return(madeMethod.Invoke(null, new[] { defName }) as Def);
        }
예제 #9
0
파일: Converter.cs 프로젝트: themotte/utils
        /// <summary>
        /// Converts an XML element to a suitable object type.
        /// </summary>
        /// <remarks>
        /// `type` is set to the expected return type; you can return null, or anything that can be implicitly converted to that type.
        ///
        /// In case of error, call Def.Dbg.Err with some appropriately useful message and return null. Message should be formatted as $"{inputName}:{(input as IXmlLineInfo).LineNumber}: Something went wrong".
        /// </remarks>
        public virtual object FromXml(XElement input, Type type, string inputName)
        {
            Dbg.Err($"{inputName}:{input.LineNumber()}: Failed to parse XML when attempting to parse {type}");

            string text = input.GetText();

            if (text != null)
            {
                // try to fall back to string?
                return(FromString(text, type, inputName, input.LineNumber()));
            }

            // oh well
            return(null);
        }
예제 #10
0
        internal static Type ParseDefFormatted(string text, string inputLine, int lineNumber)
        {
            if (ParseCache.TryGetValue(text, out Type cacheVal))
            {
                return(cacheVal);
            }

            if (Config.TestParameters?.explicitTypes != null)
            {
                // Test override, we check the test types first
                foreach (var explicitType in Config.TestParameters.explicitTypes)
                {
                    if (text == explicitType.Name)
                    {
                        ParseCache[text] = explicitType;
                        return(explicitType);
                    }
                }
            }

            // We need to find a class that matches the least number of tokens. Namespaces can't be templates so at most this continues until we hit a namespace.
            var possibleTypes = Config.UsingNamespaces
                                .Select(ns => ParseWithNamespace($"{ns}.{text}", inputLine, lineNumber))
                                .Concat(ParseWithNamespace(text, inputLine, lineNumber))
                                .Where(t => t != null)
                                .ToArray();

            Type result;

            if (possibleTypes.Length == 0)
            {
                Dbg.Err($"{inputLine}:{lineNumber}: Couldn't find type named {text}");
                result = null;
            }
            else if (possibleTypes.Length > 1)
            {
                Dbg.Err($"{inputLine}:{lineNumber}: Found too many types named {text} ({possibleTypes.Select(t => t.FullName).ToCommaString()})");
                result = possibleTypes[0];
            }
            else
            {
                result = possibleTypes[0];
            }

            ParseCache[text] = result;
            return(result);
        }
예제 #11
0
파일: Parser.cs 프로젝트: themotte/utils
        internal static void StaticReferencesInitialized()
        {
            var frame  = new StackFrame(2);
            var method = frame.GetMethod();
            var type   = method.DeclaringType;

            if (s_Status != Status.Distributing)
            {
                Dbg.Err($"Initializing static reference class {type} while the world is in {s_Status} state; should be {Status.Distributing} state - this probably means you accessed a static reference class before it was ready");
            }
            else if (!staticReferencesRegistering.Contains(type))
            {
                Dbg.Err($"Initializing static reference class {type} which was not originally detected as a static reference class");
            }

            staticReferencesRegistered.Add(type);
        }
예제 #12
0
        internal static DefDatabaseStatus GetDefDatabaseStatus(this Type type)
        {
            if (!GetDefDatabaseStatusCache.TryGetValue(type, out var result))
            {
                if (!typeof(Def).IsAssignableFrom(type))
                {
                    Dbg.Err($"Queried the def hierarchy status of a type {type} that doesn't even inherit from Def.");

                    result = DefDatabaseStatus.Invalid;
                }
                else if (type.GetCustomAttribute <AbstractAttribute>(false) != null)
                {
                    if (!type.IsAbstract)
                    {
                        Dbg.Err($"Type {type} is tagged Def.Abstract, but is not abstract.");
                    }

                    if (type.BaseType != typeof(object) && GetDefDatabaseStatus(type.BaseType) > DefDatabaseStatus.Abstract)
                    {
                        Dbg.Err($"Type {type} is tagged Def.Abstract, but inherits from {type.BaseType} which is within the database.");
                    }

                    result = DefDatabaseStatus.Abstract;
                }
                else if (type.BaseType.GetCustomAttribute <AbstractAttribute>(false) != null)
                {
                    // We do this just to validate everything beneath this. It'd better return Abstract! More importantly, it goes through all parents and makes sure they're consistent.
                    GetDefDatabaseStatus(type.BaseType);

                    result = DefDatabaseStatus.Root;
                }
                else
                {
                    // Further validation. This time we really hope it returns Abstract or Root. More importantly, it goes through all parents and makes sure they're consistent.
                    GetDefDatabaseStatus(type.BaseType);

                    // Our parent isn't NotDatabaseRootAttribute. We are not a database root, but we also can't say anything meaningful about our parents.
                    result = DefDatabaseStatus.Branch;
                }

                GetDefDatabaseStatusCache.Add(type, result);
            }

            return(result);
        }
예제 #13
0
파일: Database.cs 프로젝트: themotte/utils
        /// <summary>
        /// Renames an existing def.
        /// </summary>
        /// <remarks>
        /// This exists mostly for the Writer functionality. It is generally not recommended to use this during actual gameplay.
        /// </remarks>
        public static void Rename(Def def, string defName)
        {
            if (Get(def.GetType(), def.DefName) != def)
            {
                Dbg.Err($"Attempting to rename what used to be {def} but is no longer a registered Def");
                return;
            }

            if (def.DefName != defName && Get(def.GetType().GetDefRootType(), defName) != null)
            {
                Dbg.Err($"Attempting to rename {def} to {defName} when it already exists");
                return;
            }

            Unregister(def);
            def.DefName = defName;
            Register(def);
        }
예제 #14
0
파일: Parser.cs 프로젝트: themotte/utils
        /// <summary>
        /// Creates a Parser.
        /// </summary>
        public Parser()
        {
            if (s_Status != Status.Uninitialized)
            {
                Dbg.Err($"Parser created while the world is in {s_Status} state; should be {Status.Uninitialized} state");
            }
            s_Status = Status.Accumulating;

            bool unitTestMode = Config.TestParameters != null;

            {
                IEnumerable <Type> staticRefs;
                if (!unitTestMode)
                {
                    staticRefs = UtilReflection.GetAllTypes().Where(t => t.GetCustomAttribute <StaticReferencesAttribute>() != null);
                }
                else if (Config.TestParameters.explicitStaticRefs != null)
                {
                    staticRefs = Config.TestParameters.explicitStaticRefs;
                }
                else
                {
                    staticRefs = Enumerable.Empty <Type>();
                }

                foreach (var type in staticRefs)
                {
                    if (type.GetCustomAttribute <StaticReferencesAttribute>() == null)
                    {
                        Dbg.Err($"{type} is not tagged as StaticReferences");
                    }

                    if (!type.IsAbstract || !type.IsSealed)
                    {
                        Dbg.Err($"{type} is not static");
                    }

                    staticReferences.Add(type);
                }
            }

            Serialization.Initialize();
        }
예제 #15
0
파일: Database.cs 프로젝트: themotte/utils
        /// <summary>
        /// Creates a Def.
        /// </summary>
        /// <remarks>
        /// This will be supported for dynamically-generated Defs in the future, but right now exists mostly for the Writer functionality. It is currently not recommended to use this during actual gameplay.
        ///
        /// At this time, this will not register Indexes. This behavior may change at some point in the future.
        /// </remarks>
        public static T Create <T>(string defName) where T : Def, new()
        {
            if (Database <T> .Get(defName) != null)
            {
                Dbg.Err($"Attempting to dynamically create {typeof(T)}:{defName} when it already exists");
                return(null);
            }

            if (Get(typeof(T).GetDefRootType(), defName) != null)
            {
                Dbg.Err($"Attempting to dynamically create {typeof(T)}:{defName} when a conflicting Def already exists");
                return(null);
            }

            var defInstance = new T();

            defInstance.DefName = defName;

            Register(defInstance);

            return(defInstance);
        }
예제 #16
0
        internal static void Initialize()
        {
            Converters = new Dictionary <Type, Converter>();

            IEnumerable <Type> conversionTypes;

            if (Config.TestParameters == null)
            {
                conversionTypes = UtilReflection.GetAllTypes().Where(t => t.IsSubclassOf(typeof(Converter)) && !t.IsAbstract);
            }
            else if (Config.TestParameters.explicitConverters != null)
            {
                conversionTypes = Config.TestParameters.explicitConverters;
            }
            else
            {
                conversionTypes = Enumerable.Empty <Type>();
            }

            foreach (var type in conversionTypes)
            {
                var converter      = (Converter)System.Activator.CreateInstance(type);
                var convertedTypes = converter.HandledTypes();
                if (convertedTypes.Count == 0)
                {
                    Dbg.Err($"{type} is a Def.Converter, but doesn't convert anything");
                }

                foreach (var convertedType in convertedTypes)
                {
                    if (Converters.ContainsKey(convertedType))
                    {
                        Dbg.Err($"Converters {Converters[convertedType].GetType()} and {type} both generate result {convertedType}");
                    }

                    Converters[convertedType] = converter;
                }
            }
        }
예제 #17
0
        internal static T SingleOrDefaultChecked <T>(this IEnumerable <T> elements)
        {
            T    result = default(T);
            bool first  = true;

            foreach (var element in elements)
            {
                if (first)
                {
                    result = element;
                    first  = false;
                }
                else
                {
                    // maybe we need a better error message here.
                    Dbg.Err("Multiple items found when only one is expected");

                    // no point in continuing
                    break;
                }
            }

            return(result);
        }
예제 #18
0
파일: Converter.cs 프로젝트: themotte/utils
 /// <summary>
 /// Converts a string to a suitable object type.
 /// </summary>
 /// <remarks>
 /// `type` is set to the expected return type; you can return null, or anything that can be implicitly converted to that type.
 ///
 /// In case of error, call Def.Dbg.Err with some appropriately useful message and return null. Message should be formatted as $"{inputName}:{lineNumber}: Something went wrong".
 ///
 /// Note: In the case of empty input (i.e. &lt;member&gt;&lt;/member&gt; or &lt;member /&gt;) this function will be called.
 /// </remarks>
 public virtual object FromString(string input, Type type, string inputName, int lineNumber)
 {
     Dbg.Err($"{inputName}:{lineNumber}: Failed to parse string when attempting to parse {type}");
     return(null);
 }
예제 #19
0
        private static Type ParseWithoutNamespace(Type root, string text, string inputLine, int lineNumber)
        {
            if (root == null)
            {
                return(null);
            }

            if (text.Length == 0)
            {
                return(root);
            }

            if (text[0] == '.')
            {
                // This is a member class of a class
                int    end        = Math.Min(text.IndexOfUnbounded('<', 1), text.IndexOfUnbounded('.', 1));
                string memberName = text.Substring(1, end - 1);

                // Get our type
                Type chosenType = root.GetNestedType(memberName, BindingFlags.Public | BindingFlags.NonPublic);

                // Ho ho! We have gotten a type! It has definitely not failed!
                // Unless it's a generic type in which case it might have failed.
                if (chosenType == null)
                {
                    foreach (var prospectiveType in root.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic))
                    {
                        if (MatchesWithGeneric(prospectiveType.Name, memberName))
                        {
                            chosenType = prospectiveType;
                            break;
                        }
                    }
                }

                // Chain on to another call in case we have a further-nested class-of-class
                return(ParseWithoutNamespace(chosenType, text.Substring(end), inputLine, lineNumber));
            }
            else if (text[0] == '<')
            {
                if (!root.IsGenericTypeDefinition)
                {
                    Dbg.Err($"{inputLine}:{lineNumber}: Found generic specification on non-generic type {root}");
                    return(null);
                }

                // This is a template
                // Parsing this is going to be a bit tricky; in theory it's C#-regex-able but I haven't been able to find a good simple example
                // So we're doing it by hand
                // Which is slow
                // Definitely gonna want caching for this at some point.
                var parsedTypes = new List <Type>();
                void AddParsedType(string type)
                {
                    parsedTypes.Add(ParseDefFormatted(type.Trim(), inputLine, lineNumber));
                }

                int tokenStart = 1;
                while (tokenStart < text.Length && text[tokenStart] != '>')
                {
                    int tokenEnd = tokenStart;

                    int nestedBrackets = 0;
                    while (true)
                    {
                        // just so we can stop calling this function
                        char kar = text[tokenEnd];

                        if (kar == ',')
                        {
                            AddParsedType(text.Substring(tokenStart, tokenEnd - tokenStart));
                            tokenStart = tokenEnd + 1;
                            tokenEnd   = tokenStart;
                            continue;
                        }
                        else if (kar == '<')
                        {
                            ++nestedBrackets;
                        }
                        else if (kar == '>' && nestedBrackets > 0)
                        {
                            --nestedBrackets;
                        }
                        else if (kar == '>')
                        {
                            // we have reached the end of the templates
                            AddParsedType(text.Substring(tokenStart, tokenEnd - tokenStart));
                            tokenStart = tokenEnd;
                            break;
                        }

                        // consume another character!
                        ++tokenEnd;

                        if (tokenEnd >= text.Length)
                        {
                            // We've hit the end; this is a failure, but it'll be picked up by the below error
                            tokenStart = tokenEnd;
                            break;
                        }
                    }
                }

                if (tokenStart >= text.Length || text[tokenStart] != '>')
                {
                    Dbg.Err($"{inputLine}:{lineNumber}: Failed to find closing angle bracket in type");
                    return(null);
                }

                if (parsedTypes.Count != root.GetGenericArguments().Length)
                {
                    Dbg.Err($"{inputLine}:{lineNumber}: Wrong number of generic arguments for type {root}");
                    return(null);
                }

                // Alright, we have a valid set of brackets, and a parsed set of types!
                Type specifiedType = root.MakeGenericType(parsedTypes.ToArray());

                // yay!

                // We also might have more type to parse.
                return(ParseWithoutNamespace(specifiedType, text.Substring(tokenStart + 1), inputLine, lineNumber));
            }
            else
            {
                // nope.
                return(null);
            }
        }
예제 #20
0
파일: Converter.cs 프로젝트: themotte/utils
 /// <summary>
 /// Converts an object to a string.
 /// </summary>
 /// <remarks>
 /// `input` will be one of the types provided in HandledTypes(); it will not be null. Whatever you return should be convertable back to an object by an overridden FromString().
 ///
 /// In case of error, call Def.Dbg.Err with some appropriately useful message and return null.
 /// </remarks>
 public virtual string ToString(object input)
 {
     Dbg.Err($"Failed to generate a string when attempting to record {input.GetType()}");
     return(null);
 }
예제 #21
0
        /// <summary>
        /// Parses the output of Write, generating an object and all its related serialized data.
        /// </summary>
        public static T Read <T>(string input, string stringName = "input")
        {
            XDocument doc;

            try
            {
                doc = XDocument.Parse(input, LoadOptions.SetLineInfo);
            }
            catch (System.Xml.XmlException e)
            {
                Dbg.Ex(e);
                return(default(T));
            }

            if (doc.Elements().Count() > 1)
            {
                // This isn't testable, unfortunately; XDocument doesn't even support multiple root elements.
                Dbg.Err($"{stringName}: Found {doc.Elements().Count()} root elements instead of the expected 1");
            }

            var record = doc.Elements().First();

            if (record.Name.LocalName != "Record")
            {
                Dbg.Wrn($"{stringName}:{record.LineNumber()}: Found root element with name \"{record.Name.LocalName}\" when it should be \"Record\"");
            }

            var recordFormatVersion = record.ElementNamed("recordFormatVersion");

            if (recordFormatVersion == null)
            {
                Dbg.Err($"{stringName}:{record.LineNumber()}: Missing record format version, assuming the data is up-to-date");
            }
            else if (recordFormatVersion.GetText() != "1")
            {
                Dbg.Err($"{stringName}:{recordFormatVersion.LineNumber()}: Unknown record format version {recordFormatVersion.GetText()}, expected 1 or earlier");

                // I would rather not guess about this
                return(default(T));
            }

            var refs = record.ElementNamed("refs");

            var readerContext = new ReaderContext(stringName, true);

            if (refs != null)
            {
                // First, we need to make the instances for all the references, so they can be crosslinked appropriately
                foreach (var reference in refs.Elements())
                {
                    if (reference.Name.LocalName != "Ref")
                    {
                        Dbg.Wrn($"{stringName}:{reference.LineNumber()}: Reference element should be named 'Ref'");
                    }

                    var id = reference.Attribute("id")?.Value;
                    if (id == null)
                    {
                        Dbg.Err($"{stringName}:{reference.LineNumber()}: Missing reference ID");
                        continue;
                    }

                    var className = reference.Attribute("class")?.Value;
                    if (className == null)
                    {
                        Dbg.Err($"{stringName}:{reference.LineNumber()}: Missing reference class name");
                        continue;
                    }

                    var possibleType = (Type)Serialization.ParseString(className, typeof(Type), null, stringName, reference.LineNumber());
                    if (possibleType.IsValueType)
                    {
                        Dbg.Err($"{stringName}:{reference.LineNumber()}: Reference assigned type {possibleType}, which is a value type");
                        continue;
                    }

                    // Create a stub so other things can reference it later
                    readerContext.refs[id] = Activator.CreateInstance(possibleType);
                    if (readerContext.refs[id] == null)
                    {
                        // This is difficult to test; there are very few things that can get CreateInstance to return null, and right now the type system doesn't support them (int? for example)
                        Dbg.Err($"{stringName}:{reference.LineNumber()}: Reference of type {possibleType} was not properly created; this will cause issues");
                        continue;
                    }
                }

                // Now that all the refs exist, we can run through them again and actually parse them
                foreach (var reference in refs.Elements())
                {
                    var id = reference.Attribute("id")?.Value;
                    if (id == null)
                    {
                        // Just skip it, we don't have anything useful we can do here
                        continue;
                    }

                    // The serialization routines don't know how to deal with this, so we'll remove it now
                    reference.Attribute("id").Remove();

                    var refInstance = readerContext.refs.TryGetValue(id);
                    if (refInstance == null)
                    {
                        // We failed to parse this for some reason, so just skip it now
                        continue;
                    }

                    // Do our actual parsing
                    var refInstanceOutput = Serialization.ParseElement(reference, refInstance.GetType(), refInstance, readerContext, hasReferenceId: true);

                    if (refInstance != refInstanceOutput)
                    {
                        Dbg.Err($"{stringName}:{reference.LineNumber()}: Something really bizarre has happened and we got the wrong object back. Things are probably irrevocably broken. Please report this as a bug in Def.");
                        continue;
                    }
                }
            }

            var data = record.ElementNamed("data");

            if (data == null)
            {
                Dbg.Err($"{stringName}:{record.LineNumber()}: No data element provided. This is not very recoverable.");

                return(default(T));
            }

            // And now, we can finally parse our actual root element!
            // (which accounts for a tiny percentage of things that need to be parsed)
            return((T)Serialization.ParseElement(data, typeof(T), null, readerContext));
        }
예제 #22
0
        internal static object ParseString(string text, Type type, object model, string inputName, int lineNumber)
        {
            // Special case: Converter override
            // This is redundant if we're being called from ParseElement, but we aren't always.
            if (Converters.ContainsKey(type))
            {
                object result;

                try
                {
                    result = Converters[type].FromString(text, type, inputName, lineNumber);
                }
                catch (Exception e)
                {
                    Dbg.Ex(e);

                    if (type.IsValueType)
                    {
                        result = Activator.CreateInstance(type);
                    }
                    else
                    {
                        result = null;
                    }
                }

                if (result != null && !type.IsAssignableFrom(result.GetType()))
                {
                    Dbg.Err($"{inputName}:{lineNumber}: Converter {Converters[type].GetType()} for {type} returned unexpected type {result.GetType()}");
                    return(null);
                }

                return(result);
            }

            // Special case: defs
            if (typeof(Def).IsAssignableFrom(type))
            {
                if (type.GetDefRootType() == null)
                {
                    Dbg.Err($"{inputName}:{lineNumber}: Non-hierarchy defs cannot be used as references");
                    return(null);
                }

                if (text == "" || text == null)
                {
                    // you reference nothing, you get the null
                    return(null);
                }
                else
                {
                    Def result = Database.Get(type, text);
                    if (result == null)
                    {
                        Dbg.Err($"{inputName}:{lineNumber}: Couldn't find {type} named {text}");
                    }
                    return(result);
                }
            }

            // Special case: types
            if (type == typeof(Type))
            {
                if (text == "")
                {
                    return(null);
                }

                return(UtilType.ParseDefFormatted(text, inputName, lineNumber));
            }

            // Various non-composite-type special-cases
            if (text != "")
            {
                // If we've got text, treat us as an object of appropriate type
                try
                {
                    return(TypeDescriptor.GetConverter(type).ConvertFromString(text));
                }
                catch (System.Exception e)  // I would normally not catch System.Exception, but TypeConverter is wrapping FormatException in an Exception for some reason
                {
                    Dbg.Err($"{inputName}:{lineNumber}: {e.ToString()}");
                    return(model);
                }
            }
            else if (type == typeof(string))
            {
                // If we don't have text, and we're a string, return ""
                return("");
            }
            else
            {
                // If we don't have text, and we've fallen down to this point, that's an error (and return original value I guess)
                Dbg.Err($"{inputName}:{lineNumber}: Empty field provided for type {type}");
                return(model);
            }
        }
예제 #23
0
        internal static XElement ComposeElement(object value, Type fieldType, string label, WriterContext context, bool isRootDef = false)
        {
            var result = new XElement(label);

            // Do all our unreferencables first
            if (fieldType.IsPrimitive)
            {
                result.Add(new XText(value.ToString()));

                return(result);
            }

            if (value is string)
            {
                result.Add(new XText(value as string));

                return(result);
            }

            if (value is Type)
            {
                result.Add(new XText((value as Type).ComposeDefFormatted()));

                return(result);
            }

            if (!isRootDef && typeof(Def).IsAssignableFrom(fieldType))
            {
                // It is! Let's just get the def name and be done with it.
                if (value != null)
                {
                    var valueDef = value as Def;

                    if (valueDef != Database.Get(valueDef.GetType(), valueDef.DefName))
                    {
                        Dbg.Err("Referenced def {valueDef} no longer exists in the database; serializing an error value instead");
                        result.Add(new XText($"{valueDef.DefName}_DELETED"));
                    }
                    else
                    {
                        result.Add(new XText(valueDef.DefName));
                    }
                }

                // "No data" is defined as null for defs, so we just do that

                return(result);
            }

            // Everything after this represents "null" with an explicit XML tag, so let's just do that
            if (value == null)
            {
                result.SetAttributeValue("null", "true");
                return(result);
            }

            // Check to see if we should make this into a ref
            if (context.RecorderMode && !fieldType.IsValueType)
            {
                if (context.RegisterReference(value, result))
                {
                    // The ref system has set up the appropriate tagging, so we're done!
                    return(result);
                }

                // This is not a reference! (yet, at least). So keep on generating it.
            }

            // We'll drop through if we're in force-ref-resolve mode, or if we have something that needs conversion and is a struct (classes get turned into refs)

            // This is also where we need to start being concerned about types. If we have a type that isn't the expected type, tag it.
            if (value.GetType() != fieldType)
            {
                result.Add(new XAttribute("class", value.GetType().ComposeDefFormatted()));
            }

            if (fieldType.IsArray)
            {
                var list = value as Array;

                Type referencedType = fieldType.GetElementType();

                for (int i = 0; i < list.Length; ++i)
                {
                    result.Add(ComposeElement(list.GetValue(i), referencedType, "li", context));
                }

                return(result);
            }

            if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List <>))
            {
                var list = value as IList;

                Type referencedType = fieldType.GetGenericArguments()[0];

                for (int i = 0; i < list.Count; ++i)
                {
                    result.Add(ComposeElement(list[i], referencedType, "li", context));
                }

                return(result);
            }

            if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Dictionary <,>))
            {
                var dict = value as IDictionary;

                Type keyType   = fieldType.GetGenericArguments()[0];
                Type valueType = fieldType.GetGenericArguments()[1];

                // I really want some way to canonicalize this ordering
                IDictionaryEnumerator iterator = dict.GetEnumerator();
                while (iterator.MoveNext())
                {
                    // In theory, some dicts support inline format, not li format. Inline format is cleaner and smaller and we should be using it when possible.
                    // In practice, it's hard and I'm lazy and this always works, and we're not providing any guarantees about cleanliness of serialized output.
                    // Revisit this later when someone (possibly myself) really wants it improved.
                    var element = new XElement("li");
                    result.Add(element);

                    element.Add(ComposeElement(iterator.Key, keyType, "key", context));
                    element.Add(ComposeElement(iterator.Value, valueType, "value", context));
                }

                return(result);
            }

            if (typeof(IRecordable).IsAssignableFrom(fieldType))
            {
                var recordable = value as IRecordable;

                context.RegisterPendingWrite(() => recordable.Record(new RecorderWriter(result, context)));

                return(result);
            }

            {
                // Look for a converter; that's the only way to handle this before we fall back to reflection
                var converter = Serialization.Converters.TryGetValue(fieldType);
                if (converter != null)
                {
                    context.RegisterPendingWrite(() => converter.Record(value, fieldType, new RecorderWriter(result, context)));
                    return(result);
                }
            }

            if (context.RecorderMode)
            {
                Dbg.Err($"Couldn't find a composition method for type {fieldType}; do you need a Converter?");
                return(result);
            }

            // We absolutely should not be doing reflection when in recorder mode; that way lies madness.

            foreach (var field in value.GetType().GetFieldsFromHierarchy())
            {
                if (field.IsBackingField())
                {
                    continue;
                }

                if (field.GetCustomAttribute <IndexAttribute>() != null)
                {
                    // we don't save indices
                    continue;
                }

                if (field.GetCustomAttribute <NonSerializedAttribute>() != null)
                {
                    // we also don't save nonserialized
                    continue;
                }

                result.Add(ComposeElement(field.GetValue(value), field.FieldType, field.Name, context));
            }

            return(result);
        }
예제 #24
0
파일: Parser.cs 프로젝트: themotte/utils
        /// <summary>
        /// Finish all parsing.
        /// </summary>
        public void Finish()
        {
            if (s_Status != Status.Accumulating)
            {
                Dbg.Err($"Finishing while the world is in {s_Status} state; should be {Status.Accumulating} state");
            }
            s_Status = Status.Processing;

            // Resolve all our inheritance jobs
            foreach (var work in inheritanceJobs)
            {
                // These are the actions we need to perform; we actually have to resolve these backwards (it makes their construction a little easier)
                // The final parse is listed first, then all the children up to the final point
                var actions = new List <Action>();

                actions.Add(() => Serialization.ParseElement(work.xml, work.target.GetType(), work.target, work.context, isRootDef: true));

                string        currentDefName = work.target.DefName;
                XElement      currentXml     = work.xml;
                ReaderContext currentContext = work.context;

                string parentDefName = work.parent;
                while (parentDefName != null)
                {
                    var parentData = potentialParents.TryGetValue(Tuple.Create(work.target.GetType().GetDefRootType(), parentDefName));

                    // This is a struct for the sake of performance, so child itself won't be null
                    if (parentData.xml == null)
                    {
                        Dbg.Err($"{currentContext.sourceName}:{currentXml.LineNumber()}: Def {currentDefName} is attempting to use parent {parentDefName}, but no such def exists");

                        // Not much more we can do here.
                        break;
                    }

                    actions.Add(() => Serialization.ParseElement(parentData.xml, work.target.GetType(), work.target, parentData.context, isRootDef: true));

                    currentDefName = parentDefName;
                    currentXml     = parentData.xml;
                    currentContext = parentData.context;

                    parentDefName = parentData.parent;
                }

                finishWork.Add(() =>
                {
                    for (int i = actions.Count - 1; i >= 0; --i)
                    {
                        actions[i]();
                    }
                });
            }

            foreach (var work in finishWork)
            {
                work();
            }

            if (s_Status != Status.Processing)
            {
                Dbg.Err($"Distributing while the world is in {s_Status} state; should be {Status.Processing} state");
            }
            s_Status = Status.Distributing;

            staticReferencesRegistering.Clear();
            staticReferencesRegistering.UnionWith(staticReferences);
            foreach (var stat in staticReferences)
            {
                StaticReferencesAttribute.StaticReferencesFilled.Add(stat);

                foreach (var field in stat.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static))
                {
                    var def = Database.Get(field.FieldType, field.Name);
                    if (def == null)
                    {
                        Dbg.Err($"Failed to find {field.FieldType} named {field.Name}");
                    }
                    else if (!field.FieldType.IsAssignableFrom(def.GetType()))
                    {
                        Dbg.Err($"Static reference {field.FieldType} {stat}.{field.Name} is not compatible with {def.GetType()} {def}");
                        field.SetValue(null, null); // this is unnecessary, but it does kick the static constructor just in case we wouldn't do it otherwise
                    }
                    else
                    {
                        field.SetValue(null, def);
                    }
                }

                if (!staticReferencesRegistered.Contains(stat))
                {
                    Dbg.Err($"Failed to properly register {stat}; you may be missing a call to Def.StaticReferences.Initialized() in its static constructor");
                }
            }

            if (s_Status != Status.Distributing)
            {
                Dbg.Err($"Finalizing while the world is in {s_Status} state; should be {Status.Distributing} state");
            }
            s_Status = Status.Finalizing;

            foreach (var def in Database.List)
            {
                try
                {
                    def.ConfigErrors(err => Dbg.Err($"{def.GetType()} {def}: {err}"));
                }
                catch (Exception e)
                {
                    Dbg.Ex(e);
                }
            }

            foreach (var def in Database.List)
            {
                try
                {
                    def.PostLoad(err => Dbg.Err($"{def.GetType()} {def}: {err}"));
                }
                catch (Exception e)
                {
                    Dbg.Ex(e);
                }
            }

            if (s_Status != Status.Finalizing)
            {
                Dbg.Err($"Completing while the world is in {s_Status} state; should be {Status.Finalizing} state");
            }
            s_Status = Status.Finished;
        }
예제 #25
0
파일: Parser.cs 프로젝트: themotte/utils
        /// <summary>
        /// Pass an XML document string in for processing.
        /// </summary>
        /// <param name="stringName">A human-readable identifier useful for debugging. Generally, the name of the file that the string was read from. Not required (but very useful.)</param>
        public void AddString(string input, string stringName = "(unnamed)")
        {
            // This is a really easy error to make; we might as well handle it.
            if (input.EndsWith(".xml"))
            {
                Dbg.Err($"It looks like you've passed the filename {input} to AddString instead of the actual XML file. Either use AddFile() or pass the file contents in.");
            }

            if (s_Status != Status.Accumulating)
            {
                Dbg.Err($"Adding data while while the world is in {s_Status} state; should be {Status.Accumulating} state");
            }

            XDocument doc;

            try
            {
                doc = XDocument.Parse(input, LoadOptions.SetLineInfo);
            }
            catch (System.Xml.XmlException e)
            {
                Dbg.Ex(e);
                return;
            }

            if (doc.Elements().Count() > 1)
            {
                // This isn't testable, unfortunately; XDocument doesn't even support multiple root elements.
                Dbg.Err($"{stringName}: Found {doc.Elements().Count()} root elements instead of the expected 1");
            }

            var readerContext = new ReaderContext(stringName, false);

            foreach (var rootElement in doc.Elements())
            {
                if (rootElement.Name.LocalName != "Defs")
                {
                    Dbg.Wrn($"{stringName}:{rootElement.LineNumber()}: Found root element with name \"{rootElement.Name.LocalName}\" when it should be \"Defs\"");
                }

                foreach (var defElement in rootElement.Elements())
                {
                    string typeName = defElement.Name.LocalName;

                    Type typeHandle = UtilType.ParseDefFormatted(typeName, stringName, defElement.LineNumber());
                    if (typeHandle == null || !typeof(Def).IsAssignableFrom(typeHandle))
                    {
                        Dbg.Err($"{stringName}:{defElement.LineNumber()}: {typeName} is not a valid root Def type");
                        continue;
                    }

                    if (defElement.Attribute("defName") == null)
                    {
                        Dbg.Err($"{stringName}:{defElement.LineNumber()}: No def name provided");
                        continue;
                    }

                    string defName = defElement.Attribute("defName").Value;
                    if (!DefNameValidator.IsMatch(defName))
                    {
                        Dbg.Err($"{stringName}:{defElement.LineNumber()}: Def name \"{defName}\" doesn't match regex pattern \"{DefNameValidator}\"");
                        continue;
                    }

                    // Consume defName so we know it's not hanging around
                    defElement.Attribute("defName").Remove();

                    // Check to see if we're abstract
                    bool abstrct = false;
                    {
                        var abstractAttribute = defElement.Attribute("abstract");
                        if (abstractAttribute != null)
                        {
                            if (!bool.TryParse(abstractAttribute.Value, out abstrct))
                            {
                                Dbg.Err($"{stringName}:{defElement.LineNumber()}: Error encountered when parsing abstract attribute");
                            }

                            abstractAttribute.Remove();
                        }
                    }

                    // Get our parent info
                    string parent = null;
                    {
                        var parentAttribute = defElement.Attribute("parent");
                        if (parentAttribute != null)
                        {
                            parent = parentAttribute.Value;

                            parentAttribute.Remove();
                        }
                    }

                    // Register ourselves as an available parenting object
                    {
                        var identifier = Tuple.Create(typeHandle.GetDefRootType(), defName);
                        if (potentialParents.ContainsKey(identifier))
                        {
                            Dbg.Err($"{stringName}:{defElement.LineNumber()}: Def {identifier.Item1}:{identifier:Item2} redefined");
                        }
                        else
                        {
                            potentialParents[identifier] = new Parent {
                                xml = defElement, context = readerContext, parent = parent
                            };
                        }
                    }

                    if (!abstrct)
                    {
                        // Not abstract, so create our instance
                        var defInstance = (Def)Activator.CreateInstance(typeHandle);
                        defInstance.DefName = defName;

                        Database.Register(defInstance);

                        if (parent == null)
                        {
                            // Non-parent objects are simple; we just handle them here in order to avoid unnecessary GC churn
                            finishWork.Add(() => Serialization.ParseElement(defElement, typeHandle, defInstance, readerContext, isRootDef: true));
                        }
                        else
                        {
                            // Add an inheritance resolution job; we'll take care of this soon
                            inheritanceJobs.Add(new InheritanceJob {
                                target = defInstance, xml = defElement, context = readerContext, parent = parent
                            });
                        }
                    }
                }
            }
        }
예제 #26
0
        internal static object ParseElement(XElement element, Type type, object model, ReaderContext context, bool isRootDef = false, bool hasReferenceId = false)
        {
            // The first thing we do is parse all our attributes. This is because we want to verify that there are no attributes being ignored.
            // Don't return anything until we do our element.HasAtttributes check!

            // Figure out our intended type, if it's been overridden
            if (element.Attribute("class") != null)
            {
                var className    = element.Attribute("class").Value;
                var possibleType = (Type)ParseString(className, typeof(Type), null, context.sourceName, element.LineNumber());
                if (!type.IsAssignableFrom(possibleType))
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Explicit type {className} cannot be assigned to expected type {type}");
                }
                else if (model != null && model.GetType() != possibleType)
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Explicit type {className} does not match already-provided instance {type}");
                }
                else
                {
                    type = possibleType;
                }

                element.Attribute("class").Remove();
            }

            bool   shouldBeNull = bool.Parse(element.ConsumeAttribute("null") ?? "false");
            string refId        = element.ConsumeAttribute("ref");

            if (shouldBeNull && refId != null)
            {
                Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Element cannot be both null and a reference at the same time");

                // There's no good answer here, but we're sticking with the null because it feels like an error-handling path that the user is more likely to properly support.
                refId = null;
            }

            // See if we just want to return null
            if (shouldBeNull)
            {
                // No remaining attributes are allowed in nulls
                if (element.HasAttributes)
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Has unconsumed attributes");
                }

                // okay
                return(null);

                // Note: It may seem wrong that we can return null along with a non-null model.
                // The problem is that this is meant to be able to override defaults. If the default if an object, explicitly setting it to null *should* clear the object out.
                // If we actually need a specific object to be returned, for whatever reason, the caller has to do the comparison.
            }

            // See if we can get a ref out of it
            if (refId != null)
            {
                // No remaining attributes are allowed in refs
                if (element.HasAttributes)
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Has unconsumed attributes");
                }

                if (context == null)
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Found a reference object outside of record-reader mode");
                    return(model);
                }

                if (!context.refs.ContainsKey(refId))
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Found a reference object {refId} without a valid reference mapping");
                    return(model);
                }

                object refObject = context.refs[refId];
                if (!type.IsAssignableFrom(refObject.GetType()))
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Reference object {refId} is of type {refObject.GetType()}, which cannot be converted to expected type {type}");
                    return(model);
                }

                return(refObject);
            }

            // Converters may do their own processing, so we'll just defer off to them now; hell, you can even have both elements and text, if that's your jam
            if (Converters.ContainsKey(type))
            {
                // context might be null; that's OK at the moment
                object result;

                try
                {
                    result = Converters[type].Record(model, type, new RecorderReader(element, context));
                }
                catch (Exception e)
                {
                    Dbg.Ex(e);

                    if (model != null)
                    {
                        result = model;
                    }
                    else if (type.IsValueType)
                    {
                        result = Activator.CreateInstance(type);
                    }
                    else
                    {
                        result = null;
                    }
                }

                // This is an important check if we have a referenced type, because if we've changed the result, references won't link up to it properly.
                // Outside referenced types, it doesn't matter - we want to give people as much control over modification as possible.
                if (model != null && hasReferenceId && model != result)
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Converter {Converters[type].GetType()} for {type} ignored the model {model} while reading a referenced object; this may cause lost data");
                    return(result);
                }

                if (result != null && !type.IsAssignableFrom(result.GetType()))
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Converter {Converters[type].GetType()} for {type} returned unexpected type {result.GetType()}");
                    return(null);
                }

                return(result);
            }

            // After this point we won't be using a converter in any way, we'll be requiring native Def types (as native as it gets, at least)

            bool hasElements = element.Elements().Any();
            bool hasText     = element.Nodes().OfType <XText>().Any();

            if (typeof(Def).IsAssignableFrom(type) && hasElements && !isRootDef)
            {
                Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Inline def definitions are not currently supported");
                return(null);
            }

            // Special case: IRecordables
            if (typeof(IRecordable).IsAssignableFrom(type))
            {
                var recordable = (IRecordable)(model ?? Activator.CreateInstance(type));

                recordable.Record(new RecorderReader(element, context));

                // TODO: support indices if this is within the Def system?

                return(recordable);
            }

            // No remaining attributes are allowed past this point!
            if (element.HasAttributes)
            {
                Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Has unconsumed attributes");
            }

            // All our standard text-using options
            if (hasText ||
                (typeof(Def).IsAssignableFrom(type) && !isRootDef) ||
                type == typeof(Type) ||
                type == typeof(string) ||
                type.IsPrimitive)
            {
                if (hasElements)
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Elements are not valid when parsing {type}");
                }

                return(ParseString(element.GetText(), type, model, context.sourceName, element.LineNumber()));
            }

            // Nothing past this point even supports text, so let's just get angry and break stuff.
            if (hasText)
            {
                Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Text detected in a situation where it is invalid; will be ignored");
            }

            // Special case: Lists
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List <>))
            {
                // List<> handling
                Type referencedType = type.GetGenericArguments()[0];

                var list = (IList)(model ?? Activator.CreateInstance(type));

                // If you have a default list, but specify it in XML, we assume this is a full override. Clear the original list.
                list.Clear();

                foreach (var fieldElement in element.Elements())
                {
                    if (fieldElement.Name.LocalName != "li")
                    {
                        Dbg.Err($"{context.sourceName}:{fieldElement.LineNumber()}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
                    }

                    list.Add(ParseElement(fieldElement, referencedType, null, context));
                }

                return(list);
            }

            // Special case: Arrays
            if (type.IsArray)
            {
                Type referencedType = type.GetElementType();

                var elements = element.Elements().ToArray();

                // We don't bother falling back on model here; we probably need to recreate it anyway with the right length
                var array = (Array)Activator.CreateInstance(type, new object[] { elements.Length });

                for (int i = 0; i < elements.Length; ++i)
                {
                    var fieldElement = elements[i];
                    if (fieldElement.Name.LocalName != "li")
                    {
                        Dbg.Err($"{context.sourceName}:{fieldElement.LineNumber()}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
                    }

                    array.SetValue(ParseElement(fieldElement, referencedType, null, context), i);
                }

                return(array);
            }

            // Special case: Dictionaries
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary <,>))
            {
                // Dictionary<> handling
                Type keyType   = type.GetGenericArguments()[0];
                Type valueType = type.GetGenericArguments()[1];

                var dict = (IDictionary)(model ?? Activator.CreateInstance(type));

                // If you have a default dict, but specify it in XML, we assume this is a full override. Clear the original dict.
                dict.Clear();

                foreach (var fieldElement in element.Elements())
                {
                    if (fieldElement.Name.LocalName == "li")
                    {
                        // Treat this like a key/value pair
                        var keyNode   = fieldElement.ElementNamed("key");
                        var valueNode = fieldElement.ElementNamed("value");

                        if (keyNode == null)
                        {
                            Dbg.Err($"{context.sourceName}:{fieldElement.LineNumber()}: Dictionary includes li tag without a key");
                            continue;
                        }

                        if (valueNode == null)
                        {
                            Dbg.Err($"{context.sourceName}:{fieldElement.LineNumber()}: Dictionary includes li tag without a value");
                            continue;
                        }

                        var key = ParseElement(keyNode, keyType, null, context);

                        if (dict.Contains(key))
                        {
                            Dbg.Err($"{context.sourceName}:{fieldElement.LineNumber()}: Dictionary includes duplicate key {key.ToString()}");
                        }

                        dict[key] = ParseElement(valueNode, valueType, null, context);
                    }
                    else
                    {
                        var key = ParseString(fieldElement.Name.LocalName, keyType, null, context.sourceName, fieldElement.LineNumber());

                        if (dict.Contains(key))
                        {
                            Dbg.Err($"{context.sourceName}:{fieldElement.LineNumber()}: Dictionary includes duplicate key {fieldElement.Name.LocalName}");
                        }

                        dict[key] = ParseElement(fieldElement, valueType, null, context);
                    }
                }

                return(dict);
            }

            // At this point, we're either a class or a struct, and we need to do the reflection thing

            // If we have refs, something has gone wrong; we should never be doing reflection inside a Record system.
            // This is a really ad-hoc way of testing this and should be fixed.
            // One big problem here is that I'm OK with security vulnerabilities in def xmls. Those are either supplied by the developer or by mod authors who are intended to have full code support anyway.
            // I'm less OK with security vulnerabilities in save files. Nobody expects a savefile can compromise their system.
            // And the full reflection system is probably impossible to secure, whereas the Record system should be secureable.
            if (context.RecorderMode)
            {
                Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Falling back to reflection within a Record system; this is currently not allowed for security reasons");
                return(model);
            }

            // If we haven't been given a template class from our parent, go ahead and init to defaults
            if (model == null)
            {
                model = Activator.CreateInstance(type);
            }

            var setFields = new HashSet <string>();

            foreach (var fieldElement in element.Elements())
            {
                // Check for fields that have been set multiple times
                string fieldName = fieldElement.Name.LocalName;
                if (setFields.Contains(fieldName))
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Duplicate field {fieldName}");
                    // Just allow us to fall through; it's an error, but one with a reasonably obvious handling mechanism
                }
                setFields.Add(fieldName);

                var fieldInfo = type.GetFieldFromHierarchy(fieldName);
                if (fieldInfo == null)
                {
                    // Try to find a close match, if we can, just for a better error message
                    string match = null;
                    string canonicalFieldName = Util.LooseMatchCanonicalize(fieldName);

                    foreach (var testField in type.GetFieldsFromHierarchy())
                    {
                        if (Util.LooseMatchCanonicalize(testField.Name) == canonicalFieldName)
                        {
                            match = testField.Name;

                            // We could in theory do something overly clever where we try to find the best name, but I really don't care that much; this is meant as a quick suggestion, not an ironclad solution.
                            break;
                        }
                    }

                    if (match != null)
                    {
                        Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Field {fieldName} does not exist in type {type}; did you mean {match}?");
                    }
                    else
                    {
                        Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Field {fieldName} does not exist in type {type}");
                    }

                    continue;
                }

                if (fieldInfo.GetCustomAttribute <IndexAttribute>() != null)
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Attempting to set index field {fieldName}; these are generated by the def system");
                    continue;
                }

                if (fieldInfo.GetCustomAttribute <NonSerializedAttribute>() != null)
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Attempting to set nonserialized field {fieldName}");
                    continue;
                }

                // Check for fields we're not allowed to set
                if (UtilReflection.ReflectionSetForbidden(fieldInfo))
                {
                    Dbg.Err($"{context.sourceName}:{element.LineNumber()}: Field {fieldName} is not allowed to be set through reflection");
                    continue;
                }

                fieldInfo.SetValue(model, ParseElement(fieldElement, fieldInfo.FieldType, fieldInfo.GetValue(model), context));
            }


            // Set up our index fields; this has to happen last in case we're a struct
            Index.Register(ref model);

            return(model);
        }