using System; using System.Linq; using System.Xml; using System.ComponentModel; using System.Reflection; using System.Collections; using System.Drawing; using System.Collections.Generic; using System.IO; using System.Globalization; using JD.Core; namespace JD.Serialization { /// /// The keywords used by the VerboseXmlSerializer. /// internal static class KeyWords { public static readonly string c_valueTag = "value"; public static readonly string c_valueNameTag = "valueName"; public static readonly string c_readonlyTag = "readonly"; public static readonly string c_typeTag = "type"; public static readonly string c_enumTag = "Enum"; public static readonly string c_flagsTag = "Flags"; public static readonly string c_validValueTag = "VALIDVALUE"; public static readonly string c_itemTag = "ITEM"; public static readonly string c_countTag = "Count"; public static readonly string c_keyTag = "KEY"; public static readonly string c_valTag = "VAL"; public static readonly string c_listTag = "List"; public static readonly string c_mapTag = "Map"; public static readonly string c_multiMapTag = "MultiMap"; public static readonly string c_factoryDescriptionTag = "FACTORY_DESCRIPTION"; } /// /// XML extensions useful for the VerboseXmlSerializer. /// internal static class XmlExtensions { /// /// A value node is a node that has a "value" attribute. /// public static bool IsValueNode(this XmlNode _xmlNode) { return _xmlNode.Attributes[KeyWords.c_valueTag] != null; } /// /// Get the value of a "value node". this must be a "value node". /// public static string GetNodeValue(this XmlNode _xmlNode) { return _xmlNode.Attributes[KeyWords.c_valueTag].Value; } /// /// Case-insensitive function to get a child by name. /// public static XmlNode GetChildNodeByName(this XmlNode _xmlNode, string _childNodeName) { foreach (XmlNode child in _xmlNode.ChildNodes) { if (child.Name.Equals(_childNodeName, StringComparison.OrdinalIgnoreCase)) return child; } return null; } /// /// Get the first child that is not a "value node". Returns null otherwise. /// Note: Also skip the "factory description" child. /// public static XmlNode GetFirstNonValueChildOrNull(this XmlNode _xmlNode) { foreach (XmlNode child in _xmlNode.ChildNodes) { if (!child.IsValueNode() && !child.Name.Equals(KeyWords.c_factoryDescriptionTag, StringComparison.OrdinalIgnoreCase)) return child; } return null; } /// /// Find first child named _childNodeName and returns its value. Returns null otherwise. /// public static string GetChildNodeValue(this XmlNode _xmlNode, string _childNodeName) { XmlNode child = _xmlNode.GetChildNodeByName(_childNodeName); if (child != null) { return child.Attributes[KeyWords.c_valueTag].Value; } return null; } /// /// Find first child named _childNodeName and returns its boolean value. /// Returns false if child not found. Does not check the type of the child. /// public static bool GetChildNodeBoolValue(this XmlNode _xmlNode, string _childNodeName) { XmlNode child = _xmlNode.GetChildNodeByName(_childNodeName); if (child != null) { return child.Attributes[KeyWords.c_valueTag].Value == "1"; } return false; } /// /// Returns the boolean value of an attribute. Returns false if attribute not found. /// public static bool GetBoolAttribute(this XmlNode _xmlNode, string _attributeName) { XmlAttribute attribute = _xmlNode.Attributes[_attributeName]; return (attribute != null && attribute.Value == "1"); } public static string GetAttributeValue(this XmlNode _xmlNode, string _attribute, string _default) { XmlAttribute atr = _xmlNode.Attributes[_attribute]; return (atr == null) ? _default : atr.Value; } } /// /// An XML serializer that contains a lot of optionnal informations. There is at /// least one element per property (in the non-verbose version, properties are /// serialized as fields. /// Example: /// /// /// /// ... /// /// public class VerboseXmlSerializer : Serializer { /// /// The XML document the serializer is currently working with. /// private XmlDocument m_doc; /// /// Load in place flag: update the loaded object instead of re-creating it. /// private bool m_loadInPlace = false; /// /// The Serialize will resolve c# types based on their names. This is the Assembly list in which it will look. /// public List AssembliesToSearchForType { get; private set; } public VerboseXmlSerializer() : this(JD.Serialization.TypeMap.Instance) { } public VerboseXmlSerializer(ITypeMap alloc) : base(alloc) { AssembliesToSearchForType = new List { typeof(LuaSerializer).Assembly }; } public object LoadObjectFromXmlString(string xmlContent) { m_doc = new XmlDocument(); m_doc.LoadXml(xmlContent); return LoadObjectFromXmlDocument(); } public void FillObjectFromXmlString(object objToFill, string xmlContent) { m_doc = new XmlDocument(); m_doc.LoadXml(xmlContent); m_loadInPlace = true; try { XmlNode node = m_doc["root"]; if (node != null) { node = node.FirstChild; if (node != null) FillObjectFromXmlNode(objToFill, node); } } finally { m_loadInPlace = false; } } public string FillXmlStringFromObject(object obj) { if (BuildXmlDocumentFromObject(obj)) { XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.IndentChars = "\t"; settings.OmitXmlDeclaration = false; settings.Encoding = System.Text.Encoding.GetEncoding("ISO-8859-1"); var stringBuilder = new StringWriter(); using (XmlWriter writer = XmlTextWriter.Create(stringBuilder, settings)) { System.Xml.XPath.XPathNavigator navigator = m_doc.CreateNavigator(); writer.WriteNode(navigator, true); } return stringBuilder.ToString(); } return ""; } private Type FindType(string name) { string fullName = "JD.Serialization." + name; var result = Type.GetType(fullName); if (result == null) { foreach (var assembly in AssembliesToSearchForType) { result = assembly.GetType(fullName); if (result != null) { return result; } } } return result; } protected override object LoadObjectFromFileImplementation(string file, Type type) { m_doc = new XmlDocument(); m_doc.Load(file); return LoadObjectFromXmlDocument(); } protected object LoadObjectFromXmlDocument() { XmlNode node = m_doc["root"]; if (node != null) { node = node.FirstChild; if (node != null) return CreateObjectFromXmlNode(node); } return null; } /// /// Find the type of an XML element. Sets innerXmlNode to the child element containing the value. /// private Type GetTypeFromObjectXmlNode(XmlNode node, Type baseType, out XmlNode innerXmlNode) { // Early out for basic types if (node == null || BasicTypeList.IsBasicType(baseType)) { innerXmlNode = node; return baseType; } // First, look for the NAME element string xmlTypeName = node.GetAttributeValue(KeyWords.c_typeTag, null); // Then look at the name of the first child innerXmlNode = node.GetFirstNonValueChildOrNull(); if (xmlTypeName == null && innerXmlNode != null) xmlTypeName = innerXmlNode.Name; // Check that the type exists Type xmlType = FindType(xmlTypeName); if (xmlType != null && !baseType.IsAssignableFrom(xmlType)) throw new Exception("VerboseXmlSerializer: Cannot use type " + xmlTypeName + " as " + baseType.Name + "."); if (xmlType == null) return baseType; return xmlType; } private float ExtractFloatFromString(string str, int index) { int beginPos = 0; int endPos = str.IndexOf(' ', beginPos); while (index > 0) { if (endPos >= 0) { beginPos = endPos + 1; endPos = str.IndexOf(' ', beginPos); index--; } else { break; } } if (endPos < 0) return float.Parse(str.Substring(beginPos), CultureInfo.InvariantCulture); return float.Parse(str.Substring(beginPos, endPos - beginPos), CultureInfo.InvariantCulture); } private object CreateBaseTypeObject(string val, Type targetType) { if (targetType.IsEnum) { int result; if (!int.TryParse(val, out result)) { // The enum has been written as string // Start by extracting the name of the value in the enum int lastSeparator = val.LastIndexOf(':'); if (lastSeparator >= 0) val = val.Substring(lastSeparator + 1); // Then find the value try { return Convert.ToInt32(Enum.Parse(targetType, val)); } catch (Exception) { if (System.Attribute.IsDefined(targetType, typeof(FlagsAttribute))) result = (int)0; else result = Convert.ToInt32(Activator.CreateInstance(targetType)); } } return result; } if (targetType == typeof(bool)) return int.Parse(val, CultureInfo.InvariantCulture) != 0; if (targetType == typeof(char) || targetType == typeof(Char)) { char result; if (!char.TryParse(val, out result)) result = (char)0; return result; } if (targetType == typeof(Int16)) return Int16.Parse(val, CultureInfo.InvariantCulture); if (targetType == typeof(int) || targetType == typeof(Int32)) return int.Parse(val, CultureInfo.InvariantCulture); if (targetType == typeof(UInt16)) return UInt16.Parse(val, CultureInfo.InvariantCulture); if (targetType == typeof(uint)) return uint.Parse(val, CultureInfo.InvariantCulture); if (targetType == typeof(float) || targetType == typeof(Single)) return float.Parse(val, CultureInfo.InvariantCulture); if (targetType == typeof(double)) return double.Parse(val, CultureInfo.InvariantCulture); if (targetType == typeof(ObjectPath)) return new ObjectPath(val); if (targetType == typeof(Vec2d)) { float x = ExtractFloatFromString(val, 0); float y = ExtractFloatFromString(val, 1); return new Vec2d(x, y); } if (targetType == typeof(Vec3d)) { float x = ExtractFloatFromString(val, 0); float y = ExtractFloatFromString(val, 1); float z = ExtractFloatFromString(val, 3); return new Vec3d(x, y, z); } if (targetType == typeof(Angle)) { float degrees = float.Parse(val, CultureInfo.InvariantCulture); return new Angle((float)((double)degrees), true); } object obj = null; if (targetType == typeof(String8) || targetType == typeof(String) || targetType == typeof(Path) || targetType == typeof(Platform)) { obj = val; } else if (targetType == typeof(Color) || targetType == typeof(ColorInteger) || targetType == typeof(StringID)) { uint num; if (uint.TryParse(val, out num)) obj = num; else obj = val; } else if (targetType == typeof(LocalisationId)) { uint id = uint.Parse(val, CultureInfo.InvariantCulture); if (id > uint.MaxValue) obj = uint.MaxValue; else obj = (uint)id; } else if (targetType == typeof(Volume)) { obj = float.Parse(val, CultureInfo.InvariantCulture); } if (obj == null) return null; TypeConverter conv = TypeDescriptor.GetConverter(targetType); if (conv.CanConvertFrom(obj.GetType())) { return conv.ConvertFrom(obj); } return Convert.ChangeType(obj, targetType); } public object CreateObjectFromXmlNode(XmlNode node) { Type objType = Type.GetType("JD.Serialization." + node.Name); return CreateObjectFromXmlNode(node, TypeMap.ToRuntimeType(objType)); } public object CreateObjectFromXmlNode(XmlNode node, Type targetType) { if (BasicTypeList.IsBasicType(targetType)) { string stringValue = node.GetNodeValue(); if (stringValue != null) return CreateBaseTypeObject(stringValue, targetType); throw new Exception("VerboseXmlSerializer.CreateObjectFromXmlNode: Basic type node " + node.Name + " has no value."); } else { object obj; if (!targetType.IsAbstract) { obj = Activator.CreateInstance(targetType); FillObjectFromXmlNode(obj, node); } else { obj = null; } return obj; } } private void FillObjectFromXmlNode(object objToFill, XmlNode node) { Type objType = objToFill.GetType(); foreach (var property in SerializableFields(objType)) { string propertyName = FormatPropertyName(property); if (!ShouldSerialize(property)) continue; // Find the XML node corresponding to the property XmlNode propXmlNode = node.GetChildNodeByName(propertyName); if (propXmlNode == null) { // Values defaulting at null and missing from the XML must stay null if (property.GetValue(objToFill) != null) { TypeMap.ProcessEmptyField(objToFill, property); } } else { // Create or update the property if (BasicTypeList.IsBasicType(property.PropertyType)) { // Always create new objects for basic types object propertyValue = CreateObjectFromXmlNode(propXmlNode, property.PropertyType); property.SetValue(objToFill, propertyValue, null); } else if (IsCollection(property.PropertyType)) { // Never re-create the collections so that we can safely listen to them var collection = (IUAFCollection)property.GetValue(objToFill, null); FillCollectionFromXmlNode(collection, propXmlNode, propertyName); } else { // Fill or re-create sub-objects depending on the "load in place" flag object propertyValue = null; if (m_loadInPlace) { propertyValue = property.GetValue(objToFill, null); } XmlNode innerXmlNode; Type xmlType = GetTypeFromObjectXmlNode(propXmlNode, property.PropertyType, out innerXmlNode); if (propertyValue != null && innerXmlNode != null && propertyValue.GetType() == xmlType) { FillObjectFromXmlNode(propertyValue, innerXmlNode); } else { propertyValue = CreateObjectFromXmlNode(innerXmlNode, xmlType); property.SetValue(objToFill, propertyValue, null); } } // Set the readonly flag if (objToFill is IPropertiesMetaData) { var propertyBrowserObject = (IPropertiesMetaData)objToFill; propertyBrowserObject.SetPropertyMetaData(propertyName, "readonly", propXmlNode.GetBoolAttribute(KeyWords.c_readonlyTag)); } } } } private void FillCollectionFromXmlNode(IUAFCollection collection, XmlNode node, string collectionName) { if (!m_loadInPlace) collection.Clear(); if (collection is IUAFDictionary) { IUAFDictionary uafDictionnary = collection as IUAFDictionary; bool isBasicType = BasicTypeList.IsBasicType(uafDictionnary.ValueType); if (IsCollection(uafDictionnary.KeyType)) throw new Exception("VerboseXmlSerializer.FillCollectionFromXmlNode: Maps indexed by collections are not supported (" + collectionName + ")."); if (uafDictionnary.KeyType.IsAbstract) throw new Exception("VerboseXmlSerializer.FillCollectionFromXmlNode: Maps indexed by abstract types are not supported (" + collectionName + ")."); // Store all the keys of the collections so that we know which ones have been updated Type myKeyType = uafDictionnary.KeyType.IsEnum ? typeof(int) : uafDictionnary.KeyType; Type openKeyListType = typeof(List<>); Type genericKeyListType = openKeyListType.MakeGenericType(myKeyType); IList keysNotInXml = Activator.CreateInstance(genericKeyListType) as IList; foreach (ISerializableKeyValuePair pair in uafDictionnary.SerializableElements) keysNotInXml.Add(pair.Key); foreach (XmlNode itemNode in node.ChildNodes) { if (!itemNode.Name.Equals(KeyWords.c_itemTag, StringComparison.OrdinalIgnoreCase)) continue; XmlNode keyXmlNode = itemNode.GetChildNodeByName(KeyWords.c_keyTag); XmlNode valueXmlNode = itemNode.GetChildNodeByName(KeyWords.c_valTag); object itemKey = (keyXmlNode == null ? Activator.CreateInstance(uafDictionnary.KeyType) : CreateObjectFromXmlNode(keyXmlNode, uafDictionnary.KeyType)); bool keyAlreadyInDictionnary = uafDictionnary.ContainsKey(itemKey); XmlNode innerXmlNode; Type xmlType = GetTypeFromObjectXmlNode(valueXmlNode, uafDictionnary.ValueType, out innerXmlNode); object itemValue = null; if (keyAlreadyInDictionnary && !isBasicType) { // Get the existing object to update in the dictionnary itemValue = uafDictionnary.GetValue(itemKey); if (itemValue.GetType() != xmlType) itemValue = null; } if (itemValue != null && innerXmlNode != null) { FillObjectFromXmlNode(itemValue, innerXmlNode); } else { itemValue = (innerXmlNode != null ? CreateObjectFromXmlNode(innerXmlNode, xmlType) : Activator.CreateInstance(uafDictionnary.ValueType)); if (keyAlreadyInDictionnary) uafDictionnary.SetValue(itemKey, itemValue); // Update item else uafDictionnary.AddValue(itemKey, itemValue); // Add new item } if (innerXmlNode != null) //uafDictionnary.SetPropertyMetaData(itemKey, PropertiesMetaDataName.ReadOnly, innerXmlNode.GetBoolAttribute(KeyWords.c_readonlyTag)); uafDictionnary.SetPropertyMetaData(itemKey, "readonly", innerXmlNode.GetBoolAttribute(KeyWords.c_readonlyTag)); keysNotInXml.Remove(itemKey); } // Remove items that are not present in the lua table foreach (object key in keysNotInXml) uafDictionnary.RemoveKey(key); } else if (collection is IUAFList) { IUAFList uafList = collection as IUAFList; Type itemType = collection.ElementType; bool isBasicType = BasicTypeList.IsBasicType(itemType); int index = 0; foreach (XmlNode itemNode in node.ChildNodes) { if (!itemNode.Name.Equals(KeyWords.c_itemTag, StringComparison.OrdinalIgnoreCase)) continue; object uafListItem = null; if (index < uafList.Count) uafListItem = uafList.GetAt(index); bool itemAlreadyInList = (uafListItem != null); // For basic types, we have a "VAL" child node. XmlNode valueNode = isBasicType ? itemNode.GetChildNodeByName(KeyWords.c_valTag) : itemNode; if (valueNode == null) throw new Exception("VerboseXmlSerializer.FillCollectionFromXmlNode: items of list" + collectionName + " have no \"VAL\" but they should have one."); XmlNode innerXmlNode; Type xmlType = GetTypeFromObjectXmlNode(valueNode, itemType, out innerXmlNode); if (uafListItem != null && uafListItem.GetType() != xmlType) uafListItem = null; if (uafListItem != null && innerXmlNode != null) { FillObjectFromXmlNode(uafListItem, innerXmlNode); } else { uafListItem = (innerXmlNode != null ? CreateObjectFromXmlNode(innerXmlNode, xmlType) : Activator.CreateInstance(itemType)); if (itemAlreadyInList) uafList.SetAt(index, uafListItem); // Update item else uafList.Add(uafListItem); // Add new item } if (innerXmlNode != null) //uafList.SetPropertyMetaData(index, PropertiesMetaDataName.ReadOnly, innerXmlNode.GetBoolAttribute(KeyWords.c_readonlyTag)); uafList.SetPropertyMetaData(index, "readonly", innerXmlNode.GetBoolAttribute(KeyWords.c_readonlyTag)); ++index; } // Remove items that are not present in the lua table while (index < uafList.Count) { uafList.RemoveAt(index); ++index; } } else { throw new Exception("LuaSerializer.FillCollectionFromLuaTable: Unknown collection type."); } } protected bool BuildXmlDocumentFromObject(object obj) { m_doc = new XmlDocument(); XmlNode node = m_doc.CreateElement("root"); XmlDeclaration xmlDeclaration = m_doc.CreateXmlDeclaration("1.0", "ISO-8859-1", null); m_doc.InsertBefore(xmlDeclaration, m_doc.DocumentElement); m_doc.AppendChild(node); try { AppendObjectToXml(obj, node); return true; } catch (Exception) { return false; } } protected override bool SaveObjectToFileImplementation(object obj, string file) { if (BuildXmlDocumentFromObject(obj)) { XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.IndentChars = "\t"; settings.OmitXmlDeclaration = false; settings.Encoding = System.Text.Encoding.GetEncoding("ISO-8859-1"); using (XmlWriter writer = XmlTextWriter.Create(file, settings)) { System.Xml.XPath.XPathNavigator navigator = m_doc.CreateNavigator(); writer.WriteNode(navigator, true); } return true; } return false; } protected void AddXmlAttribute(XmlNode xmlNode, string attributeName, string attributeValue) { XmlAttribute valueAttribute = m_doc.CreateAttribute(attributeName); valueAttribute.Value = attributeValue; xmlNode.Attributes.Append(valueAttribute); } protected XmlNode AddChildNode(XmlNode parentNode, string childName) { XmlNode childNode = m_doc.CreateElement(childName); parentNode.AppendChild(childNode); return childNode; } protected void AppendObjectToXml(object obj, XmlNode parentNode) { if (obj == null) return; XmlNode childNode = m_doc.CreateElement(obj.GetType().Name); parentNode.AppendChild(childNode); FillXmlFromObject(obj, childNode); } protected void AppendBasicTypeXml(string propertyName, object propertyValue, XmlNode parentNode) { string itemNodeValue = ConvertBaseTypeToString(propertyValue); XmlNode valueNode = m_doc.CreateElement(propertyName); parentNode.AppendChild(valueNode); AddXmlAttribute(valueNode, KeyWords.c_valueTag, itemNodeValue); } protected void FillXmlFromObject(object obj, XmlNode node) { if (obj == null) return; Type objType = obj.GetType(); PropertyInfo[] fields = objType.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (PropertyInfo property in fields) { string propertyName = FormatPropertyName(property); if (!ShouldSerialize(property)) continue; Type propertyType = property.PropertyType; object propertyValue = property.GetValue(obj, null); if (propertyValue == null) continue; if (!ShouldSerialize(property, propertyValue)) continue; if (BasicTypeList.IsBasicType(propertyType)) { AppendBasicTypeXml(propertyName, propertyValue, node); } else if (IsCollection(propertyType)) { var collection = (IUAFCollection)propertyValue; Type itemType = collection.ElementType; bool basicItemType = BasicTypeList.IsBasicType(itemType); XmlNode childNode = AddChildNode(node, propertyName); AddXmlAttribute(childNode, KeyWords.c_countTag, collection.Count.ToString()); foreach (object item in collection.SerializableElements) { XmlNode itemNode = AddChildNode(childNode, KeyWords.c_itemTag); ISerializableKeyValuePair serializableItem = item as ISerializableKeyValuePair; if (serializableItem != null) { AddXmlAttribute(itemNode, KeyWords.c_typeTag, serializableItem.Value.ToString()); } if (basicItemType) { AppendBasicTypeXml(KeyWords.c_valTag, item, itemNode); } else { // If the collection is a dictionnary, we know that the items are a // pair of some kind. We don't want the pair element to be // written in the XML, we only want the KEY and the VAL elements so // we use FillXmlFromObject if (collection is IUAFDictionary) { FillXmlFromObject(item, itemNode); } else { AddXmlAttribute(itemNode, KeyWords.c_typeTag, item.GetType().Name); AppendObjectToXml(item, itemNode); } } } } else { XmlNode childNode = AddChildNode(node, propertyName); AddXmlAttribute(childNode, KeyWords.c_typeTag, propertyValue.GetType().Name); AppendObjectToXml(propertyValue, childNode); } } } private string ConvertBaseTypeToString(object obj) { if (obj.GetType().IsEnum) { return (Convert.ToInt32(obj)).ToString(); } if (obj is Vec2d) { Vec2d vec = (Vec2d)obj; return $"{vec.X} {vec.Y}".Replace(',', '.'); } if (obj is Vec3d) { Vec3d vec = (Vec3d)obj; return $"{vec.X} {vec.Y} {vec.Z}".Replace(',', '.'); } if (obj is Color) { Color col = (Color)obj; return $"0x{col.A:X2}{col.R:X2}{col.G:X2}{col.B:X2}"; } if (obj is Angle) { return $"{((Angle)obj).ToDegree()}"; } if (obj is bool) { return (bool)obj ? "1" : "0"; } if (obj is char) { if ((char)obj == '\0') return ""; return obj.ToString(); } if (obj is float || obj is double || obj is decimal) { return obj.ToString().Replace(",", "."); } return obj.ToString(); } /// /// Checking if the property ParentBind is null because root actors can't have parentBind /// /// /// /// public bool ShouldSerialize(PropertyInfo propertyInfo, object propertyValue) { return propertyInfo.Name != nameof(Actor.ParentBind) || !(propertyValue is Bind bind) || bind.ParentActor != ""; } } }