/// <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)); }
/// <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 }); } } } } }