using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; using System.Xml.Linq; namespace Barotrauma { public enum IsPropertySaveable { Yes, No } [AttributeUsage(AttributeTargets.Property)] public sealed class Serialize : Attribute { public readonly object DefaultValue; public readonly IsPropertySaveable IsSaveable; public readonly Identifier TranslationTextTag; /// /// If set to true, the instance values saved in a submarine file will always override the prefab values, even if using a mod that normally overrides instance values. /// public bool AlwaysUseInstanceValues; public string Description; /// /// Makes the property serializable to/from XML /// /// The property is set to this value during deserialization if the value is not defined in XML. /// Is the value saved to XML when serializing. /// 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. /// If set to true, the instance values saved in a submarine file will always override the prefab values, even if using a mod that normally overrides instance values. /// 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) public Serialize(object defaultValue, IsPropertySaveable isSaveable, string description = "", string translationTextTag = "", bool alwaysUseInstanceValues = false) { DefaultValue = defaultValue; IsSaveable = isSaveable; TranslationTextTag = translationTextTag.ToIdentifier(); Description = description; AlwaysUseInstanceValues = alwaysUseInstanceValues; } } [AttributeUsage(AttributeTargets.Property)] public sealed class Header : Attribute { public readonly LocalizedString Text; public Header(string text = "", string localizedTextTag = null) { Text = localizedTextTag != null ? TextManager.Get(localizedTextTag) : text; } } public sealed class SerializableProperty { private static readonly ImmutableDictionary supportedTypes = new Dictionary { { typeof(bool), "bool" }, { typeof(int), "int" }, { typeof(float), "float" }, { typeof(string), "string" }, { typeof(Identifier), "identifier" }, { typeof(LanguageIdentifier), "languageidentifier" }, { typeof(LocalizedString), "localizedstring" }, { typeof(Point), "point" }, { typeof(Vector2), "vector2" }, { typeof(Vector3), "vector3" }, { typeof(Vector4), "vector4" }, { typeof(Rectangle), "rectangle" }, { typeof(Color), "color" }, { typeof(string[]), "stringarray" }, { typeof(Identifier[]), "identifierarray" } }.ToImmutableDictionary(); private static readonly Dictionary> cachedProperties = new Dictionary>(); public readonly string Name; public readonly AttributeCollection Attributes; public readonly Type PropertyType; public readonly bool OverridePrefabValues; public readonly PropertyInfo PropertyInfo; public SerializableProperty(PropertyDescriptor property) { Name = property.Name; PropertyInfo = property.ComponentType.GetProperty(property.Name); PropertyType = property.PropertyType; Attributes = property.Attributes; OverridePrefabValues = GetAttribute()?.AlwaysUseInstanceValues ?? false; } public T GetAttribute() where T : Attribute { foreach (Attribute a in Attributes) { if (a is T) return (T)a; } return default; } 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}\" to {value}", e); return false; } } else { DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value} (Type \"{PropertyType.Name}\" not supported)"); return false; } } try { switch (typeName) { case "bool": bool boolValue = value.ToIdentifier() == "true"; if (TrySetBoolValueWithoutReflection(parentObject, boolValue)) { return true; } PropertyInfo.SetValue(parentObject, boolValue, null); break; case "int": if (int.TryParse(value, out int intVal)) { if (TrySetFloatValueWithoutReflection(parentObject, intVal)) { return true; } PropertyInfo.SetValue(parentObject, intVal, null); } else { return false; } break; case "float": if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatVal)) { if (TrySetFloatValueWithoutReflection(parentObject, floatVal)) { return true; } PropertyInfo.SetValue(parentObject, floatVal, null); } else { return false; } 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; case "identifier": PropertyInfo.SetValue(parentObject, value.ToIdentifier()); break; case "languageidentifier": PropertyInfo.SetValue(parentObject, value.ToLanguageIdentifier()); break; case "localizedstring": PropertyInfo.SetValue(parentObject, new RawLString(value)); break; case "stringarray": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value)); break; case "identifierarray": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseIdentifierArray(value)); break; } } catch (Exception e) { DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}", 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} (Type \"{PropertyType.Name}\" 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; case "identifier": PropertyInfo.SetValue(parentObject, new Identifier((string)value)); return true; case "languageidentifier": PropertyInfo.SetValue(parentObject, ((string)value).ToLanguageIdentifier()); return true; case "localizedstring": PropertyInfo.SetValue(parentObject, new RawLString((string)value)); return true; case "stringarray": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value)); return true; case "identifierarray": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseIdentifierArray((string)value)); return true; default: DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}"); DebugConsole.ThrowError($"(Cannot convert a string to a {PropertyType})"); return false; } } else if (PropertyType != value.GetType()) { DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}"); 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}\" to {value}", e); return false; } return true; } catch (Exception e) { DebugConsole.ThrowError($"Error in SerializableProperty.TrySetValue (Property: {PropertyInfo.Name})", e); return false; } } public bool TrySetValue(object parentObject, float value) { try { if (TrySetFloatValueWithoutReflection(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 (Property: {PropertyInfo.Name})", e); return false; } return true; } public bool TrySetValue(object parentObject, bool value) { try { if (TrySetBoolValueWithoutReflection(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 (Property: {PropertyInfo.Name})", e); return false; } return true; } public bool TrySetValue(object parentObject, int value) { try { if (TrySetFloatValueWithoutReflection(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 (Property: {PropertyInfo.Name})", 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.GetValue", e); return false; } } public float GetFloatValue(object parentObject) { if (parentObject == null || PropertyInfo == null) { return 0.0f; } if (TryGetFloatValueWithoutReflection(parentObject, out float value)) { return value; } try { if (PropertyType == typeof(int)) { return (int)PropertyInfo.GetValue(parentObject, null); } else { return (float)PropertyInfo.GetValue(parentObject, null); } } catch (TargetInvocationException e) { DebugConsole.ThrowError("Exception thrown by the target of SerializableProperty.GetValue", e.InnerException); return 0.0f; } catch (Exception e) { DebugConsole.ThrowError("Error in SerializableProperty.GetValue", e); return 0.0f; } } public bool GetBoolValue(object parentObject) { if (parentObject == null || PropertyInfo == null) { return false; } if (TryGetBoolValueWithoutReflection(parentObject, out bool value)) { return value; } try { return (bool)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.GetValue", 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; } private readonly ImmutableDictionary> valueGetters = new Dictionary>() { {"Voltage".ToIdentifier(), (obj) => obj is Powered p ? p.Voltage : (object) null}, {"Charge".ToIdentifier(), (obj) => obj is PowerContainer p ? p.Charge : (object) null}, {"Overload".ToIdentifier(), (obj) => obj is PowerTransfer p ? p.Overload : (object) null}, {"AvailableFuel".ToIdentifier(), (obj) => obj is Reactor r ? r.AvailableFuel : (object) null}, {"FissionRate".ToIdentifier(), (obj) => obj is Reactor r ? r.FissionRate : (object) null}, {"OxygenFlow".ToIdentifier(), (obj) => obj is Vent v ? v.OxygenFlow : (object) null}, { "CurrFlow".ToIdentifier(), (obj) => obj is Pump p ? (object) p.CurrFlow : obj is OxygenGenerator o ? (object)o.CurrFlow : null }, {"CurrentVolume".ToIdentifier(), (obj) => obj is Engine e ? e.CurrentVolume : (object)null}, {"MotionDetected".ToIdentifier(), (obj) => obj is MotionSensor m ? m.MotionDetected : (object)null}, {"Oxygen".ToIdentifier(), (obj) => obj is Character c ? c.Oxygen : (object)null}, {"Health".ToIdentifier(), (obj) => obj is Character c ? c.Health : (object)null}, {"OxygenAvailable".ToIdentifier(), (obj) => obj is Character c ? c.OxygenAvailable : (object)null}, {"PressureProtection".ToIdentifier(), (obj) => obj is Character c ? c.PressureProtection : (object)null}, {"IsDead".ToIdentifier(), (obj) => obj is Character c ? c.IsDead : (object)null}, {"IsHuman".ToIdentifier(), (obj) => obj is Character c ? c.IsHuman : (object)null}, {"IsOn".ToIdentifier(), (obj) => obj is LightComponent l ? l.IsOn : (object)null}, {"Condition".ToIdentifier(), (obj) => obj is Item i ? i.Condition : (object)null}, {"ContainerIdentifier".ToIdentifier(), (obj) => obj is Item i ? i.ContainerIdentifier : (object)null}, {"PhysicsBodyActive".ToIdentifier(), (obj) => obj is Item i ? i.PhysicsBodyActive : (object)null}, }.ToImmutableDictionary(); /// /// Try getting the values of some commonly used properties directly without reflection /// private object TryGetValueWithoutReflection(object parentObject) { if (PropertyType == typeof(float)) { if (TryGetFloatValueWithoutReflection(parentObject, out float value)) { return value; } } else if (PropertyType == typeof(bool)) { if (TryGetBoolValueWithoutReflection(parentObject, out bool value)) { return value; } } else if (PropertyType == typeof(string)) { if (TryGetStringValueWithoutReflection(parentObject, out string value)) { return value; } } return null; } /// /// Try getting the values of some commonly used properties directly without reflection /// private bool TryGetFloatValueWithoutReflection(object parentObject, out float value) { value = 0.0f; switch (Name) { case nameof(Powered.Voltage): { if (parentObject is Powered powered) { value = powered.Voltage; return true; } } break; case nameof(Powered.RelativeVoltage): { if (parentObject is Powered powered) { value = powered.RelativeVoltage; return true; } } break; case nameof(Powered.CurrPowerConsumption): { if (parentObject is Powered powered) { value = powered.CurrPowerConsumption; return true; } } break; case nameof(PowerContainer.Charge): { if (parentObject is PowerContainer powerContainer) { value = powerContainer.Charge; return true; } } break; case nameof(PowerContainer.ChargePercentage): { if (parentObject is PowerContainer powerContainer) { value = powerContainer.ChargePercentage; return true; } } break; case nameof(PowerContainer.RechargeRatio): { if (parentObject is PowerContainer powerContainer) { value = powerContainer.RechargeRatio; return true; } } break; case nameof(Reactor.AvailableFuel): { if (parentObject is Reactor reactor) { value = reactor.AvailableFuel; return true; } } break; case nameof(Reactor.FissionRate): { if (parentObject is Reactor reactor) { value = reactor.FissionRate; return true; } } break; case nameof(Reactor.Temperature): { if (parentObject is Reactor reactor) { value = reactor.Temperature; return true; } } break; case nameof(Vent.OxygenFlow): if (parentObject is Vent vent) { value = vent.OxygenFlow; return true; } break; case nameof(Pump.CurrFlow): { if (parentObject is Pump pump) { value = pump.CurrFlow; return true; } } if (parentObject is OxygenGenerator oxygenGenerator) { value = oxygenGenerator.CurrFlow; return true; } break; case nameof(Engine.CurrentBrokenVolume): { if (parentObject is Engine engine) { value = engine.CurrentBrokenVolume; return true; } } { if (parentObject is Pump pump) { value = pump.CurrentBrokenVolume; return true; } } break; case nameof(Engine.CurrentVolume): { if (parentObject is Engine engine) { value = engine.CurrentVolume; return true; } } break; case nameof(Character.Oxygen): { if (parentObject is Character character) { value = character.Oxygen; return true; } } { if (parentObject is Hull hull) { value = hull.Oxygen; return true; } } break; case nameof(Character.Health): { if (parentObject is Character character) { value = character.Health; return true; } } break; case nameof(Character.OxygenAvailable): { if (parentObject is Character character) { value = character.OxygenAvailable; return true; } } break; case nameof(Character.PressureProtection): { if (parentObject is Character character) { value = character.PressureProtection; return true; } } break; case nameof(Item.Condition): { if (parentObject is Item item) { value = item.Condition; return true; } } break; case nameof(Character.SpeedMultiplier): { if (parentObject is Character character) { value = character.SpeedMultiplier; return true; } } break; case nameof(Character.PropulsionSpeedMultiplier): { if (parentObject is Character character) { value = character.PropulsionSpeedMultiplier; return true; } } break; case nameof(Character.LowPassMultiplier): { if (parentObject is Character character) { value = character.LowPassMultiplier; return true; } } break; case nameof(Character.HullOxygenPercentage): { if (parentObject is Character character) { value = character.HullOxygenPercentage; return true; } else if (parentObject is Item item) { value = item.HullOxygenPercentage; return true; } } break; case nameof(Door.Stuck): { if (parentObject is Door door) { value = door.Stuck; return true; } } break; } return false; } /// /// Try getting the values of some commonly used properties directly without reflection /// private bool TryGetBoolValueWithoutReflection(object parentObject, out bool value) { value = false; switch (Name) { case nameof(ItemComponent.IsActive): if (parentObject is ItemComponent ic) { value = ic.IsActive; return true; } break; case nameof(PowerTransfer.Overload): if (parentObject is PowerTransfer powerTransfer) { value = powerTransfer.Overload; return true; } break; case nameof(MotionSensor.MotionDetected): if (parentObject is MotionSensor motionSensor) { value = motionSensor.MotionDetected; return true; } break; case nameof(Character.IsDead): { if (parentObject is Character character) { value = character.IsDead; return true; } } break; case nameof(Character.IsHuman): { if (parentObject is Character character) { value = character.IsHuman; return true; } } break; case nameof(LightComponent.IsOn): { if (parentObject is LightComponent lightComponent) { value = lightComponent.IsOn; return true; } } break; case nameof(Item.PhysicsBodyActive): { if (parentObject is Item item) { value = item.PhysicsBodyActive; return true; } } break; case nameof(DockingPort.Docked): if (parentObject is DockingPort dockingPort) { value = dockingPort.Docked; return true; } break; case nameof(Reactor.TemperatureCritical): if (parentObject is Reactor reactor) { value = reactor.TemperatureCritical; return true; } break; case nameof(TriggerComponent.TriggerActive): if (parentObject is TriggerComponent trigger) { value = trigger.TriggerActive; return true; } break; case nameof(Controller.State): if (parentObject is Controller controller) { value = controller.State; return true; } break; case nameof(Character.InWater): { if (parentObject is Character character) { value = character.InWater; return true; } else if (parentObject is Item item) { value = item.InWater; return true; } } break; case nameof(Rope.Snapped): if (parentObject is Rope rope) { value = rope.Snapped; return true; } break; } return false; } /// /// Try getting the values of some commonly used properties directly without reflection /// private bool TryGetStringValueWithoutReflection(object parentObject, out string value) { value = null; switch (Name) { case nameof(Item.ContainerIdentifier): { if (parentObject is Item item) { value = item.ContainerIdentifier.Value; return true; } } break; } return false; } /// /// Try setting the values of some commonly used properties directly without reflection /// private bool TrySetFloatValueWithoutReflection(object parentObject, float value) { switch (Name) { case nameof(Item.Condition): { if (parentObject is Item item) { item.Condition = value; return true; } } break; case nameof(Powered.Voltage): if (parentObject is Powered powered) { powered.Voltage = value; return true; } break; case nameof(PowerContainer.Charge): if (parentObject is PowerContainer powerContainer) { powerContainer.Charge = value; return true; } break; case nameof(Reactor.AvailableFuel): if (parentObject is Reactor reactor) { reactor.AvailableFuel = value; return true; } break; case nameof(Character.Oxygen): { if (parentObject is Character character) { character.Oxygen = value; return true; } } break; case nameof(Character.OxygenAvailable): { if (parentObject is Character character) { character.OxygenAvailable = value; return true; } } break; case nameof(Character.PressureProtection): { if (parentObject is Character character) { character.PressureProtection = value; return true; } } break; case nameof(Character.LowPassMultiplier): { if (parentObject is Character character) { character.LowPassMultiplier = value; return true; } } break; case nameof(Character.SpeedMultiplier): { if (parentObject is Character character) { character.StackSpeedMultiplier(value); return true; } } break; case nameof(Character.HealthMultiplier): { if (parentObject is Character character) { character.StackHealthMultiplier(value); return true; } } break; case nameof(Character.PropulsionSpeedMultiplier): { if (parentObject is Character character) { character.PropulsionSpeedMultiplier = value; return true; } } break; case nameof(Character.ObstructVisionAmount): { if (parentObject is Character character) { character.ObstructVisionAmount = value; return true; } } break; case nameof(Item.Scale): { if (parentObject is Item item) { item.Scale = value; return true; } } break; } return false; } /// /// Try setting the values of some commonly used properties directly without reflection /// private bool TrySetBoolValueWithoutReflection(object parentObject, bool value) { switch (Name) { case nameof(Character.ObstructVision): { if (parentObject is Character character) { character.ObstructVision = value; return true; } } break; case nameof(Character.HideFace): { if (parentObject is Character character) { character.HideFace = value; return true; } } break; case nameof(Character.UseHullOxygen): { if (parentObject is Character character) { character.UseHullOxygen = value; return true; } } break; case nameof(LightComponent.IsOn): { if (parentObject is LightComponent lightComponent) { lightComponent.IsOn = value; return true; } } break; case nameof(ItemComponent.IsActive): { if (parentObject is ItemComponent ic) { ic.IsActive = 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); dictionary.Add(serializableProperty.Name.ToIdentifier(), 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.NameAsIdentifier(), 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, bool ignoreEditable = 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 == IsPropertySaveable.Yes && !attribute.DefaultValue.Equals(value)) || (!ignoreEditable && 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; case "stringarray": string[] stringArray = (string[])value; stringValue = stringArray != null ? string.Join(';', stringArray) : ""; break; case "identifierarray": Identifier[] identifierArray = (Identifier[])value; stringValue = identifierArray != null ? string.Join(';', identifierArray) : ""; break; default: stringValue = value.ToString(); break; } } element.GetAttribute(property.Name)?.Remove(); element.SetAttributeValue(property.Name, stringValue); } } /// /// Upgrade the properties of an entity saved with an older version of the game. Properties that should be upgraded are defined using "Upgrade" elements in the config file. /// for example, would force the scale of the entity to 0.5 if it was saved with a version prior to 0.9.2.0. /// /// The entity to upgrade /// The XML element to get the upgrade instructions from (e.g. the config of an item prefab) /// The game version the entity was saved with public static void UpgradeGameVersion(ISerializableEntity entity, ContentXElement configElement, Version savedVersion) { foreach (var subElement in configElement.Elements()) { if (!subElement.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase)) { continue; } var upgradeVersion = new Version(subElement.GetAttributeString("gameversion", "0.0.0.0")); if (subElement.GetAttributeBool("campaignsaveonly", false)) { if ((GameMain.GameSession?.LastSaveVersion ?? GameMain.Version) >= upgradeVersion) { continue; } } else { if (savedVersion >= upgradeVersion) { continue; } } foreach (XAttribute attribute in subElement.Attributes()) { var attributeName = attribute.NameAsIdentifier(); if (attributeName == "gameversion" || attributeName == "campaignsaveonly") { continue; } if (attributeName == "refreshrect") { if (entity is Structure structure) { if (!structure.ResizeHorizontal) { structure.Rect = structure.DefaultRect = new Rectangle(structure.Rect.X, structure.Rect.Y, (int)structure.Prefab.ScaledSize.X, structure.Rect.Height); } if (!structure.ResizeVertical) { structure.Rect = structure.DefaultRect = new Rectangle(structure.Rect.X, structure.Rect.Y, structure.Rect.Width, (int)structure.Prefab.ScaledSize.Y); } } else if (entity is Item item) { if (!item.ResizeHorizontal) { item.Rect = item.DefaultRect = new Rectangle(item.Rect.X, item.Rect.Y, (int)(item.Prefab.Size.X * item.Prefab.Scale), item.Rect.Height); } if (!item.ResizeVertical) { item.Rect = item.DefaultRect = new Rectangle(item.Rect.X, item.Rect.Y, item.Rect.Width, (int)(item.Prefab.Size.Y * item.Prefab.Scale)); } } } if (entity.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) { FixValue(property, entity, attribute); if (property.Name == nameof(ItemComponent.Msg) && entity is ItemComponent component) { component.ParseMsg(); } } else if (entity is Item item1) { foreach (ISerializableEntity component in item1.AllPropertyObjects) { if (component.SerializableProperties.TryGetValue(attributeName, out SerializableProperty componentProperty)) { FixValue(componentProperty, component, attribute); if (componentProperty.Name == nameof(ItemComponent.Msg)) { ((ItemComponent)component).ParseMsg(); } } } } } static void FixValue(SerializableProperty property, object parentObject, XAttribute attribute) { if (attribute.Value.Length > 0 && attribute.Value[0] == '*') { float.TryParse(attribute.Value.Substring(1), NumberStyles.Float, CultureInfo.InvariantCulture, out float multiplier); if (property.PropertyType == typeof(int)) { property.TrySetValue(parentObject, (int)(((int)property.GetValue(parentObject)) * multiplier)); } else if (property.PropertyType == typeof(float)) { property.TrySetValue(parentObject, (float)property.GetValue(parentObject) * multiplier); } else if (property.PropertyType == typeof(Vector2)) { property.TrySetValue(parentObject, (Vector2)property.GetValue(parentObject) * multiplier); } else if (property.PropertyType == typeof(Point)) { property.TrySetValue(parentObject, ((Point)property.GetValue(parentObject)).Multiply(multiplier)); } } else if (attribute.Value.Length > 0 && attribute.Value[0] == '+') { if (property.PropertyType == typeof(int)) { float.TryParse(attribute.Value.Substring(1), NumberStyles.Float, CultureInfo.InvariantCulture, out float addition); property.TrySetValue(parentObject, (int)(((int)property.GetValue(parentObject)) + addition)); } else if (property.PropertyType == typeof(float)) { float.TryParse(attribute.Value.Substring(1), NumberStyles.Float, CultureInfo.InvariantCulture, out float addition); property.TrySetValue(parentObject, (float)property.GetValue(parentObject) + addition); } else if (property.PropertyType == typeof(Vector2)) { var addition = XMLExtensions.ParseVector2(attribute.Value.Substring(1)); property.TrySetValue(parentObject, (Vector2)property.GetValue(parentObject) + addition); } else if (property.PropertyType == typeof(Point)) { var addition = XMLExtensions.ParsePoint(attribute.Value.Substring(1)); property.TrySetValue(parentObject, ((Point)property.GetValue(parentObject)) + addition); } } else { property.TrySetValue(parentObject, attribute.Value); } } if (entity is Item item2) { var componentElement = subElement.FirstElement(); if (componentElement == null) { continue; } ItemComponent itemComponent = item2.Components.First(c => c.Name == componentElement.Name.ToString()); if (itemComponent == null) { continue; } foreach (XAttribute attribute in componentElement.Attributes()) { var attributeName = attribute.NameAsIdentifier(); if (itemComponent.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) { FixValue(property, itemComponent, attribute); } } foreach (var element in componentElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) { case "requireditem": case "requireditems": itemComponent.RequiredItems.Clear(); itemComponent.DisabledRequiredItems.Clear(); itemComponent.SetRequiredItems(element, allowEmpty: true); break; } } if (itemComponent is ItemContainer itemContainer && (componentElement.GetChildElement("containable") != null || componentElement.GetChildElement("subcontainer") != null)) { itemContainer.ReloadContainableRestrictions(componentElement); } } } } } }