Files
2025-09-17 13:44:21 +03:00

1203 lines
57 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using RestSharp.Extensions;
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
{
/// <summary>
/// Is the value of the property saved when saving (serializing) the entity?
/// Can be set to false if e.g. the value doesn't ever change from the prefab value, or if changes to it shouldn't persist between rounds.
/// </summary>
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;
/// <summary>
/// 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.
/// </summary>
public bool AlwaysUseInstanceValues;
public string Description;
/// <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.
/// <param name="alwaysUseInstanceValues">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)</param>
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<Type, string> supportedTypes = new Dictionary<Type, string>
{
{ 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<Type, Dictionary<Identifier, SerializableProperty>> cachedProperties =
new Dictionary<Type, Dictionary<Identifier, SerializableProperty>>();
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<Serialize>()?.AlwaysUseInstanceValues ?? false;
}
public T GetAttribute<T>() 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, ParseStringArray(value));
break;
case "identifierarray":
PropertyInfo.SetValue(parentObject, 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;
}
private static string[] ParseStringArray(string stringArrayValues)
{
return string.IsNullOrEmpty(stringArrayValues) ? Array.Empty<string>() : stringArrayValues.Split(';');
}
private static Identifier[] ParseIdentifierArray(string stringArrayValues)
{
return ParseStringArray(stringArrayValues).ToIdentifiers();
}
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, ParseStringArray((string)value));
return true;
case "identifierarray":
PropertyInfo.SetValue(parentObject, 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<Identifier, Func<object, object>> valueGetters =
new Dictionary<Identifier, Func<object, object>>()
{
{"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();
/// <summary>
/// Try getting the values of some commonly used properties directly without reflection
/// </summary>
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;
}
/// <summary>
/// Try getting the values of some commonly used properties directly without reflection
/// </summary>
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(Repairable.StressDeteriorationMultiplier):
{
if (parentObject is Repairable repairable) { value = repairable.StressDeteriorationMultiplier; 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(ItemContainer.ContainedNonBrokenItemCount):
{
if (parentObject is ItemContainer itemContainer) { value = itemContainer.ContainedNonBrokenItemCount; 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(Item.ConditionPercentage):
{ if (parentObject is Item item) { value = item.ConditionPercentage; return true; } }
break;
case nameof(Item.SightRange):
{ if (parentObject is Item item) { value = item.SightRange; return true; } }
break;
case nameof(Item.SoundRange):
{ if (parentObject is Item item) { value = item.SoundRange; 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.ObstructVisionAmount):
{ if (parentObject is Character character) { value = character.ObstructVisionAmount; 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;
}
/// <summary>
/// Try getting the values of some commonly used properties directly without reflection
/// </summary>
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(PowerContainer.OutputDisabled):
if (parentObject is PowerContainer powerContainer) { value = powerContainer.OutputDisabled; 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.NeedsAir):
{ if (parentObject is Character character) { value = character.NeedsAir; return true; } }
break;
case nameof(Character.NeedsOxygen):
{ if (parentObject is Character character) { value = character.NeedsOxygen; 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(Holdable.Attached):
if (parentObject is Holdable holdable) { value = holdable.Attached; 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;
}
/// <summary>
/// Try getting the values of some commonly used properties directly without reflection
/// </summary>
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;
}
/// <summary>
/// Try setting the values of some commonly used properties directly without reflection
/// </summary>
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;
case nameof(Item.SightRange):
{ if (parentObject is Item item) { item.SightRange = value; return true; } }
break;
case nameof(Item.SoundRange):
{ if (parentObject is Item item) { item.SoundRange = value; return true; } }
break;
}
return false;
}
/// <summary>
/// Try setting the values of some commonly used properties directly without reflection
/// </summary>
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<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<Identifier, SerializableProperty> GetProperties(object obj)
{
Type objType = obj.GetType();
if (cachedProperties.ContainsKey(objType))
{
return cachedProperties[objType];
}
var properties = TypeDescriptor.GetProperties(obj.GetType()).Cast<PropertyDescriptor>();
Dictionary<Identifier, SerializableProperty> dictionary = new Dictionary<Identifier, SerializableProperty>();
foreach (var property in properties)
{
//if the getter is private, we must get it from the declaring type to access it and check if it exists
SerializableProperty serializableProperty = null;
try
{
serializableProperty = new SerializableProperty(property);
}
catch (AmbiguousMatchException)
{
//can happen e.g. with AnimController.CurrentGroundedParams, which is of an abstract type -
//let's just ignore these types of properties (you can't really do anything with SerializableProperties that are reference types anyway)
continue;
}
dictionary.Add(serializableProperty.Name.ToIdentifier(), serializableProperty);
}
cachedProperties[objType] = dictionary;
return dictionary;
}
public static Dictionary<Identifier, SerializableProperty> DeserializeProperties(object obj, XElement element = null)
{
Dictionary<Identifier, SerializableProperty> dictionary = GetProperties(obj);
#if DEBUG
var nonPublicProperties = obj.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var property in nonPublicProperties)
{
if (property.GetAttribute<Serialize>() != null)
{
DebugConsole.ThrowError($"The property {property.Name} in class {obj.GetType()} is set as serializable, but isn't public. Serializable properties must have at least a public getter.");
}
}
#endif
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.NameAsIdentifier(), 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, bool ignoreEditable = 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 == IsPropertySaveable.Yes && !attribute.DefaultValue.Equals(value)) ||
(!ignoreEditable && 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;
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);
}
}
/// <summary>
/// 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, <Upgrade gameversion="0.9.2.0" scale="0.5"/> would force the scale of the entity to 0.5 if it was saved with a version prior to 0.9.2.0.
/// </summary>
/// <param name="entity">The entity to upgrade</param>
/// <param name="configElement">The XML element to get the upgrade instructions from (e.g. the config of an item prefab)</param>
/// <param name="savedVersion">The game version the entity was saved with</param>
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));
}
}
}
else if (attributeName == "unlockrecipe" || attributeName == "unlockrecipes")
{
var recipes = subElement.GetAttributeIdentifierImmutableHashSet("unlockrecipes",
def: subElement.GetAttributeIdentifierImmutableHashSet("unlockrecipe", ImmutableHashSet<Identifier>.Empty));
foreach (var recipe in recipes)
{
GameMain.GameSession?.UnlockRecipe(CharacterTeamType.Team1, recipe, showNotifications: false);
}
}
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.FirstOrDefault(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);
}
}
}
}
}
}