public string Write() { var doc = new XDocument(); var record = new XElement("Defs"); doc.Add(record); var writerContext = new WriterContext(false); foreach (var defObj in Database.List) { var defXml = Serialization.ComposeElement(defObj, defObj.GetType(), defObj.GetType().ComposeDefFormatted(), writerContext, isRootDef: true); defXml.Add(new XAttribute("defName", defObj.DefName)); record.Add(defXml); } writerContext.DequeuePendingWrites(); return(doc.ToString()); }
/// <summary> /// Returns a fully-formed XML document starting at an object. /// </summary> public static string Write <T>(T target, bool pretty = true) { var doc = new XDocument(); var record = new XElement("Record"); doc.Add(record); record.Add(new XElement("recordFormatVersion", 1)); var refs = new XElement("refs"); record.Add(refs); var writerContext = new WriterContext(true); var rootElement = Serialization.ComposeElement(target, target != null ? target.GetType() : typeof(T), "data", writerContext); record.Add(rootElement); // Handle all our pending writes writerContext.DequeuePendingWrites(); // We now have a giant XML tree, potentially many thousands of nodes deep, where some nodes are references and some *should* be in the reference bank but aren't. // We need to do two things: // * Make all of our tagged references into actual references in the Refs section // * Tag anything deeper than a certain depth as a reference, then move it into the Refs section var depthTestsPending = new List <XElement>(); depthTestsPending.Add(rootElement); // This is a loop between "write references" and "tag everything below a certain depth as needing to be turned into a reference". // We do this in a loop so we don't have to worry about ironically blowing our stack while making a change required to not blow our stack. while (true) { // Canonical ordering to provide some stability and ease-of-reading. foreach (var reference in writerContext.StripAndOutputReferences().OrderBy(kvp => kvp.Key)) { refs.Add(reference.Value); depthTestsPending.Add(reference.Value); } bool found = false; for (int i = 0; i < depthTestsPending.Count; ++i) { // Magic number should probably be configurable at some point found |= writerContext.ProcessDepthLimitedReferences(depthTestsPending[i], 20); } depthTestsPending.Clear(); if (!found) { // No new depth-clobbering references found, just move on break; } } if (refs.IsEmpty) { // strip out the refs 'cause it looks better that way :V refs.Remove(); } return(doc.ToString()); }
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); }