Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs
2019-07-27 20:20:43 +03:00

667 lines
27 KiB
C#

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 string translationTextTag;
/// <summary>
/// Makes the property serializable to/from XML
/// </summary>
/// <param name="defaultValue">The property is set to this value during deserialization if the value is not defined in XML.</param>
/// <param name="isSaveable">Is the value saved to XML when serializing.</param>
/// <param name="translationTextTag">If set to anything else than null, SerializableEntityEditors will show what the text gets translated to or warn if the text is not found in the language files.
/// Setting the value to a non-empty string will let the user select the text from one whose tag starts with the given string (e.g. RoomName. would show all texts with a RoomName.* tag)</param>
public Serialize(object defaultValue, bool isSaveable, string translationTextTag = null)
{
this.defaultValue = defaultValue;
this.isSaveable = isSaveable;
this.translationTextTag = translationTextTag;
}
}
class SerializableProperty
{
private static Dictionary<Type, string> supportedTypes = new Dictionary<Type, string>
{
{ 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<Type, Dictionary<string, SerializableProperty>> cachedProperties =
new Dictionary<Type, Dictionary<string, SerializableProperty>>();
private readonly PropertyInfo propertyInfo;
public readonly string Name;
public readonly string NameToLowerInvariant;
public readonly AttributeCollection Attributes;
public readonly Type PropertyType;
public PropertyInfo PropertyInfo
{
get { return propertyInfo; }
}
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<T>() 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;
}
/// <summary>
/// Try getting the values of some commonly used properties directly without reflection
/// </summary>
private object TryGetValueWithoutReflection(object parentObject)
{
switch (Name)
{
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;
case "IsOn":
{ if (parentObject is LightComponent lightComponent) { return lightComponent.IsOn; } }
break;
case "Condition":
{
if (parentObject is Item item) { return item.Condition; }
}
break;
case "ContainerIdentifier":
{
if (parentObject is Item item) { return item.ContainerIdentifier; }
}
break;
case "PhysicsBodyActive":
{
if (parentObject is Item item) { return item.PhysicsBodyActive; }
}
break;
}
return null;
}
/// <summary>
/// Try setting the values of some commonly used properties directly without reflection
/// </summary>
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;
case "IsOn":
{ if (parentObject is LightComponent lightComponent && value is bool) { lightComponent.IsOn = (bool)value; return true; } }
break;
}
return false;
}
public static List<SerializableProperty> GetProperties<T>(ISerializableEntity obj)
{
List<SerializableProperty> editableProperties = new List<SerializableProperty>();
foreach (var property in obj.SerializableProperties.Values)
{
if (property.Attributes.OfType<T>().Any()) editableProperties.Add(property);
}
return editableProperties;
}
public static Dictionary<string, SerializableProperty> GetProperties(object obj)
{
Type objType = obj.GetType();
if (cachedProperties.ContainsKey(objType))
{
return cachedProperties[objType];
}
var properties = TypeDescriptor.GetProperties(obj.GetType()).Cast<PropertyDescriptor>();
Dictionary<string, SerializableProperty> dictionary = new Dictionary<string, SerializableProperty>();
foreach (var property in properties)
{
var serializableProperty = new SerializableProperty(property, obj);
dictionary.Add(serializableProperty.NameToLowerInvariant, serializableProperty);
}
cachedProperties[objType] = dictionary;
return dictionary;
}
public static Dictionary<string, SerializableProperty> DeserializeProperties(object obj, XElement element = null)
{
Dictionary<string, SerializableProperty> 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<Serialize>())
{
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<Serialize>().Any()) { continue; }
property.TrySetValue(obj, attribute.Value);
}
}
return dictionary;
}
public static void SerializeProperties(ISerializableEntity obj, XElement element, bool saveIfDefault = false)
{
var saveProperties = GetProperties<Serialize>(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<Serialize>())
{
if ((attribute.isSaveable && !attribute.defaultValue.Equals(value)) ||
property.Attributes.OfType<Editable>().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);
}
}
}
}