using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; using System.Xml.Linq; namespace Barotrauma { [AttributeUsage(AttributeTargets.Property)] public class Editable : Attribute { public int MaxLength; public int DecimalCount = 1; public int MinValueInt = int.MinValue, MaxValueInt = int.MaxValue; public float MinValueFloat = float.MinValue, MaxValueFloat = float.MaxValue; public float ValueStep; public string ToolTip; public string DisplayName; public Editable(int maxLength = 20) { MaxLength = maxLength; } public Editable(int minValue, int maxValue) { MinValueInt = minValue; MaxValueInt = maxValue; } public Editable(float minValue, float maxValue, int decimals = 1) { MinValueFloat = minValue; MaxValueFloat = maxValue; DecimalCount = decimals; } } [AttributeUsage(AttributeTargets.Property)] public class InGameEditable : Editable { } [AttributeUsage(AttributeTargets.Property)] public class Serialize : Attribute { public object defaultValue; public bool isSaveable; public Serialize(object defaultValue, bool isSaveable) { this.defaultValue = defaultValue; this.isSaveable = isSaveable; } } class SerializableProperty { private static Dictionary supportedTypes = new Dictionary { { typeof(bool), "bool" }, { typeof(int), "int" }, { typeof(float), "float" }, { typeof(string), "string" }, { typeof(Point), "point" }, { typeof(Vector2), "vector2" }, { typeof(Vector3), "vector3" }, { typeof(Vector4), "vector4" }, { typeof(Rectangle), "rectangle" }, { typeof(Color), "color" }, }; private static readonly Dictionary> cachedProperties = new Dictionary>(); private readonly PropertyInfo propertyInfo; public readonly string Name; public readonly string NameToLowerInvariant; public readonly AttributeCollection Attributes; public readonly Type PropertyType; public SerializableProperty(PropertyDescriptor property, object obj) { Name = property.Name; NameToLowerInvariant = Name.ToLowerInvariant(); propertyInfo = property.ComponentType.GetProperty(property.Name); PropertyType = property.PropertyType; Attributes = property.Attributes; } public T GetAttribute() where T : Attribute { foreach (Attribute a in Attributes) { if (a is T) return (T)a; } return default(T); } public void SetValue(object parentObject, object val) { propertyInfo.SetValue(parentObject, val); } public bool TrySetValue(object parentObject, string value) { if (value == null) { return false; } if (!supportedTypes.TryGetValue(PropertyType, out string typeName)) { if (PropertyType.IsEnum) { object enumVal; try { enumVal = Enum.Parse(propertyInfo.PropertyType, value, true); } catch (Exception e) { DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value + " (not a valid " + propertyInfo.PropertyType + ")", e); return false; } try { propertyInfo.SetValue(parentObject, enumVal); } catch (Exception e) { DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString(), e); return false; } } else { DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value); DebugConsole.ThrowError("(Type not supported)"); return false; } } try { switch (typeName) { case "bool": bool boolValue = value == "true" || value == "True"; if (TrySetValueWithoutReflection(parentObject, boolValue)) { return true; } propertyInfo.SetValue(parentObject, boolValue, null); break; case "int": int intVal; if (int.TryParse(value, out intVal)) { if (TrySetValueWithoutReflection(parentObject, intVal)) { return true; } propertyInfo.SetValue(parentObject, intVal, null); } break; case "float": float floatVal; if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out floatVal)) { if (TrySetValueWithoutReflection(parentObject, floatVal)) { return true; } propertyInfo.SetValue(parentObject, floatVal, null); } break; case "string": propertyInfo.SetValue(parentObject, value, null); break; case "point": propertyInfo.SetValue(parentObject, XMLExtensions.ParsePoint(value)); break; case "vector2": propertyInfo.SetValue(parentObject, XMLExtensions.ParseVector2(value)); break; case "vector3": propertyInfo.SetValue(parentObject, XMLExtensions.ParseVector3(value)); break; case "vector4": propertyInfo.SetValue(parentObject, XMLExtensions.ParseVector4(value)); break; case "color": propertyInfo.SetValue(parentObject, XMLExtensions.ParseColor(value)); break; case "rectangle": propertyInfo.SetValue(parentObject, XMLExtensions.ParseRect(value, true)); break; } } catch (Exception e) { DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString(), e); return false; } return true; } public bool TrySetValue(object parentObject, object value) { if (value == null || parentObject == null || propertyInfo == null) return false; try { if (!supportedTypes.TryGetValue(PropertyType, out string typeName)) { if (PropertyType.IsEnum) { object enumVal; try { enumVal = Enum.Parse(propertyInfo.PropertyType, value.ToString(), true); } catch (Exception e) { DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value + " (not a valid " + propertyInfo.PropertyType + ")", e); return false; } propertyInfo.SetValue(parentObject, enumVal); return true; } else { DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value); DebugConsole.ThrowError("(Type not supported)"); return false; } } try { if (value.GetType() == typeof(string)) { switch (typeName) { case "string": propertyInfo.SetValue(parentObject, value, null); return true; case "point": propertyInfo.SetValue(parentObject, XMLExtensions.ParsePoint((string)value)); return true; case "vector2": propertyInfo.SetValue(parentObject, XMLExtensions.ParseVector2((string)value)); return true; case "vector3": propertyInfo.SetValue(parentObject, XMLExtensions.ParseVector3((string)value)); return true; case "vector4": propertyInfo.SetValue(parentObject, XMLExtensions.ParseVector4((string)value)); return true; case "color": propertyInfo.SetValue(parentObject, XMLExtensions.ParseColor((string)value)); return true; case "rectangle": propertyInfo.SetValue(parentObject, XMLExtensions.ParseRect((string)value, false)); return true; default: DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString()); DebugConsole.ThrowError("(Cannot convert a string to a " + PropertyType.ToString() + ")"); return false; } } else if (PropertyType != value.GetType()) { DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString()); DebugConsole.ThrowError("(Non-matching type, should be " + PropertyType + " instead of " + value.GetType() + ")"); return false; } propertyInfo.SetValue(parentObject, value, null); } catch (Exception e) { DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString(), e); return false; } return true; } catch (Exception e) { DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); return false; } } public bool TrySetValue(object parentObject, float value) { try { if (TrySetValueWithoutReflection(parentObject, value)) { return true; } propertyInfo.SetValue(parentObject, value, null); } catch (TargetInvocationException e) { DebugConsole.ThrowError("Exception thrown by the target of SerializableProperty.TrySetValue", e.InnerException); return false; } catch (Exception e) { DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); return false; } return true; } public bool TrySetValue(object parentObject, bool value) { try { if (TrySetValueWithoutReflection(parentObject, value)) { return true; } propertyInfo.SetValue(parentObject, value, null); } catch (TargetInvocationException e) { DebugConsole.ThrowError("Exception thrown by the target of SerializableProperty.TrySetValue", e.InnerException); return false; } catch (Exception e) { DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); return false; } return true; } public bool TrySetValue(object parentObject, int value) { try { propertyInfo.SetValue(parentObject, value, null); } catch (TargetInvocationException e) { DebugConsole.ThrowError("Exception thrown by the target of SerializableProperty.TrySetValue", e.InnerException); return false; } catch (Exception e) { DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); return false; } return true; } public object GetValue(object parentObject) { if (parentObject == null || propertyInfo == null) { return false; } var value = TryGetValueWithoutReflection(parentObject); if (value != null) { return value; } try { return propertyInfo.GetValue(parentObject, null); } catch (TargetInvocationException e) { DebugConsole.ThrowError("Exception thrown by the target of SerializableProperty.GetValue", e.InnerException); return false; } catch (Exception e) { DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); return false; } } public static string GetSupportedTypeName(Type type) { if (type.IsEnum) return "Enum"; if (!supportedTypes.TryGetValue(type, out string typeName)) { return null; } return typeName; } /// /// Try getting the values of some commonly used properties directly without reflection /// private object TryGetValueWithoutReflection(object parentObject) { switch (Name) { case "Condition": if (parentObject is Item item) { return item.Condition; } break; case "Voltage": if (parentObject is Powered powered) { return powered.Voltage; } break; case "Charge": if (parentObject is PowerContainer powerContainer) { return powerContainer.Charge; } break; case "AvailableFuel": { if (parentObject is Reactor reactor) { return reactor.AvailableFuel; } } break; case "FissionRate": { if (parentObject is Reactor reactor) { return reactor.FissionRate; } } break; case "OxygenFlow": if (parentObject is Vent vent) { return vent.OxygenFlow; } break; case "CurrFlow": if (parentObject is Pump pump) { return pump.CurrFlow; } if (parentObject is OxygenGenerator oxygenGenerator) { return oxygenGenerator.CurrFlow; } break; case "CurrentVolume": if (parentObject is Engine engine) { return engine.CurrentVolume; } break; case "MotionDetected": if (parentObject is MotionSensor motionSensor) { return motionSensor.MotionDetected; } break; case "Oxygen": { if (parentObject is Character character) { return character.Oxygen; } } break; case "Health": { if (parentObject is Character character) { return character.Health; } } break; case "OxygenAvailable": { if (parentObject is Character character) { return character.OxygenAvailable; } } break; case "PressureProtection": { if (parentObject is Character character) { return character.PressureProtection; } } break; case "IsDead": { if (parentObject is Character character) { return character.IsDead; } } break; } return null; } /// /// Try setting the values of some commonly used properties directly without reflection /// private bool TrySetValueWithoutReflection(object parentObject, object value) { switch (Name) { case "Condition": if (parentObject is Item item && value is float) { item.Condition = (float)value; return true; } break; case "Voltage": if (parentObject is Powered powered && value is float) { powered.Voltage = (float)value; return true; } break; case "Charge": if (parentObject is PowerContainer powerContainer && value is float) { powerContainer.Charge = (float)value; return true; } break; case "AvailableFuel": if (parentObject is Reactor reactor && value is float) { reactor.AvailableFuel = (float)value; return true; } break; case "Oxygen": { if (parentObject is Character character && value is float) { character.Oxygen = (float)value; return true; } } break; case "HideFace": { if (parentObject is Character character && value is bool) { character.HideFace = (bool)value; return true; } } break; case "OxygenAvailable": { if (parentObject is Character character && value is float) { character.OxygenAvailable = (float)value; return true; } } break; case "ObstructVision": { if (parentObject is Character character && value is bool) { character.ObstructVision = (bool)value; return true; } } break; case "PressureProtection": { if (parentObject is Character character && value is float) { character.PressureProtection = (float)value; return true; } } break; case "LowPassMultiplier": { if (parentObject is Character character && value is float) { character.LowPassMultiplier = (float)value; return true; } } break; case "SpeedMultiplier": { if (parentObject is Character character && value is float) { character.SpeedMultiplier = (float)value; return true; } } break; } return false; } public static List GetProperties(ISerializableEntity obj) { List editableProperties = new List(); foreach (var property in obj.SerializableProperties.Values) { if (property.Attributes.OfType().Any()) editableProperties.Add(property); } return editableProperties; } public static Dictionary GetProperties(object obj) { Type objType = obj.GetType(); if (cachedProperties.ContainsKey(objType)) { return cachedProperties[objType]; } var properties = TypeDescriptor.GetProperties(obj.GetType()).Cast(); Dictionary dictionary = new Dictionary(); foreach (var property in properties) { var serializableProperty = new SerializableProperty(property, obj); dictionary.Add(serializableProperty.NameToLowerInvariant, serializableProperty); } cachedProperties[objType] = dictionary; return dictionary; } public static Dictionary DeserializeProperties(object obj, XElement element = null) { Dictionary dictionary = GetProperties(obj); foreach (var property in dictionary.Values) { //set the value of the property to the default value if there is one foreach (var ini in property.Attributes.OfType()) { property.TrySetValue(obj, ini.defaultValue); break; } } if (element != null) { //go through all the attributes in the xml element //and set the value of the matching property if it is initializable foreach (XAttribute attribute in element.Attributes()) { if (!dictionary.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) { continue; } if (!property.Attributes.OfType().Any()) { continue; } property.TrySetValue(obj, attribute.Value); } } return dictionary; } public static void SerializeProperties(ISerializableEntity obj, XElement element, bool saveIfDefault = false) { var saveProperties = GetProperties(obj); foreach (var property in saveProperties) { object value = property.GetValue(obj); if (value == null) continue; if (!saveIfDefault) { //only save // - if the attribute is saveable and it's different from the default value // - or can be changed in-game or in the editor bool save = false; foreach (var attribute in property.Attributes.OfType()) { if ((attribute.isSaveable && !attribute.defaultValue.Equals(value)) || property.Attributes.OfType().Any()) { save = true; break; } } if (!save) continue; } string stringValue; if (!supportedTypes.TryGetValue(value.GetType(), out string typeName)) { if (property.PropertyType.IsEnum) { stringValue = value.ToString(); } else { DebugConsole.ThrowError("Failed to serialize the property \"" + property.Name + "\" of \"" + obj + "\" (type " + property.PropertyType + " not supported)"); continue; } } else { switch (typeName) { case "float": //make sure the decimal point isn't converted to a comma or anything else stringValue = ((float)value).ToString("G", CultureInfo.InvariantCulture); break; case "point": stringValue = XMLExtensions.PointToString((Point)value); break; case "vector2": stringValue = XMLExtensions.Vector2ToString((Vector2)value); break; case "vector3": stringValue = XMLExtensions.Vector3ToString((Vector3)value); break; case "vector4": stringValue = XMLExtensions.Vector4ToString((Vector4)value); break; case "color": stringValue = XMLExtensions.ColorToString((Color)value); break; case "rectangle": stringValue = XMLExtensions.RectToString((Rectangle)value); break; default: stringValue = value.ToString(); break; } } element.Attribute(property.Name)?.Remove(); element.SetAttributeValue(property.NameToLowerInvariant, stringValue); } } } }