#nullable enable using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; // ReSharper disable ArrangeThisQualifier namespace Barotrauma { internal sealed class PropertyReference { public object? OriginalValue { get; private set; } public readonly Identifier Name; private readonly string Multiplier; private static readonly char[] prefixCharacters = { '=', '/', '*', 'x', '-', '+' }; private readonly Upgrade upgrade; private PropertyReference(Identifier name, string multiplier, Upgrade upgrade) { this.Name = name; this.Multiplier = multiplier; this.upgrade = upgrade; } public void SetOriginalValue(object value) { OriginalValue ??= value; } /// /// Calculate the new value of the property /// /// level of the upgrade /// public object CalculateUpgrade(int level) { switch (OriginalValue) { case float _: case int _: case double _: { var value = Convert.ToSingle(OriginalValue); return level == 0 ? value : CalculateUpgrade(value, level, Multiplier); } case bool _ when bool.TryParse(Multiplier, out bool result): { return result; } default: { DebugConsole.AddWarning($"Original value of \"{Name}\" in the upgrade \"{upgrade.Prefab.Name}\" is not a integer, float, double or boolean but {OriginalValue?.GetType()} with a value of ({OriginalValue}). \n" + "The value has been assumed to be '0', did you forget a Convert.ChangeType()?"); break; } } return 0; } public static float CalculateUpgrade(float value, int level, string multiplier) { if (multiplier[^1] != '%') { return CalculateUpgradeFloat(multiplier, value , level); } return ApplyPercentage(value, UpgradePrefab.ParsePercentage(multiplier, Identifier.Empty, suppressWarnings: true), level); } private static float CalculateUpgradeFloat(string multiplier, float value, int level) { float multiplierFloat = ParseValue(multiplier, value); switch (multiplier[0]) { case '*': case 'x': return value * (multiplierFloat * level); case '/': return value / (multiplierFloat * level); case '-': return value - (multiplierFloat * level); case '+': return value + (multiplierFloat * level); case '=': return multiplierFloat; } return 0; } /// /// Sets the OriginalValue to a value stored in the save XML element /// /// public void ApplySavedValue(XElement? savedElement) { if (savedElement == null) { return; } foreach (var savedValue in savedElement.Elements()) { if (savedValue.NameAsIdentifier() == Name) { string value = savedValue.GetAttributeString("value", string.Empty); if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out float floatValue)) { OriginalValue = floatValue; } else if (bool.TryParse(value, out bool boolValue)) { OriginalValue = boolValue; } else { OriginalValue = value; } } } } /// /// Recursively apply a percentage to a value certain amount of times /// /// original value /// percentage increase/decrease /// how many times to apply the percentage change /// private static float ApplyPercentage(float value, float amount, int times) { return (1f + (amount / 100f * times)) * value; } public static PropertyReference[] ParseAttributes(IEnumerable attributes, Upgrade upgrade) { return attributes.Select(attribute => new PropertyReference(attribute.NameAsIdentifier(), attribute.Value, upgrade)).ToArray(); } private static float ParseValue(string multiplier, object? originalValue) { if (multiplier.Length > 1) { if (prefixCharacters.Contains(multiplier[0])) { if (float.TryParse(multiplier.Substring(1).Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out float value)) { return value; } if (originalValue is float || originalValue is int || originalValue is double) { return (float) originalValue; } } } return 1; } } internal sealed class Upgrade : IDisposable { private ISerializableEntity TargetEntity { get; } public Dictionary TargetComponents { get; } public UpgradePrefab Prefab { get; } public Identifier Identifier => Prefab.Identifier; public int Level { get; set; } public bool Disposed { get; private set; } private readonly ContentXElement sourceElement; public Upgrade(ISerializableEntity targetEntity, UpgradePrefab prefab, int level, XContainer? saveElement = null) { this.TargetEntity = targetEntity; this.sourceElement = prefab.SourceElement; this.Prefab = prefab; this.Level = level; var targetProperties = new Dictionary(); List? saveElements = saveElement?.Elements().ToList(); foreach (var subElement in prefab.SourceElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "decorativesprite": case "sprite": case "price": break; case "item": case "structure": case "base": case "root": case "this": XElement? savedRootElement = saveElements?.Find(e => string.Equals(e.Name.ToString(), "This", StringComparison.OrdinalIgnoreCase)); var rootProperties = PropertyReference.ParseAttributes(subElement.Attributes(), this); targetProperties.Add(targetEntity, rootProperties); foreach (var propertyRef in rootProperties) { propertyRef.ApplySavedValue(savedRootElement); } break; default: { if (targetEntity is Item item) { ISerializableEntity[]? itemComponents = FindItemComponent(item, subElement.Name.ToString()); if (itemComponents != null && itemComponents.Any()) { foreach (ISerializableEntity sEntity in itemComponents) { XElement? savedElement = saveElements?.Find(e => string.Equals(e.Name.ToString(), sEntity.Name, StringComparison.OrdinalIgnoreCase)); PropertyReference[] properties = PropertyReference.ParseAttributes(subElement.Attributes(), this); foreach (PropertyReference propertyRef in properties) { propertyRef.ApplySavedValue(savedElement); } targetProperties.Add(sEntity, properties); } } } break; } } } TargetComponents = targetProperties; if (saveElement != null) { ResetNonAffectedProperties(saveElement); } } /// /// Finds saved properties in the XML element and resets properties that are not managed by the upgrade anymore to their default values /// /// XML save element private void ResetNonAffectedProperties(XContainer saveElement) { foreach (var element in saveElement.Elements().Elements()) { if (TargetComponents.SelectMany(pair => pair.Value) .Select(@ref => @ref.Name) .Any(@identifier => @identifier == element.NameAsIdentifier())) { continue; } string value = element.GetAttributeString("value", string.Empty); Identifier name = element.NameAsIdentifier(); XElement parentElement = element.Parent ?? throw new NullReferenceException("Unable to reset properties: Parent element is null."); string componentName = parentElement.Name.ToString(); DebugConsole.AddWarning($"Upgrade \"{Prefab.Name}\" in {TargetEntity.Name} does not affect the property \"{name}\" but the save file suggest it has done so before (has it been overriden?). \n" + $"The property has been reset to the original value of {value} and will be ignored from now on."); if (string.Equals(componentName, "This", StringComparison.OrdinalIgnoreCase)) { if (TargetEntity.SerializableProperties.TryGetValue(name, out SerializableProperty? property)) { property?.SetValue(TargetEntity, Convert.ChangeType(value, property!.GetValue(TargetEntity).GetType(), NumberFormatInfo.InvariantInfo)); } } else if (TargetEntity is Item item) { ISerializableEntity[]? foundComponents = FindItemComponent(item, componentName); if (foundComponents == null) { continue; } foreach (var serializableEntity in foundComponents) { if (serializableEntity.SerializableProperties.TryGetValue(name, out SerializableProperty? property)) { property?.SetValue(serializableEntity, Convert.ChangeType(value, property!.GetValue(serializableEntity).GetType(), NumberFormatInfo.InvariantInfo)); } } } } } /// /// Find an item component matching the XML element /// /// Target item /// XML ItemComponent element /// Array of matching ItemComponents or null private static ISerializableEntity[]? FindItemComponent(Item item, string name) { Type? type = Type.GetType($"Barotrauma.Items.Components.{name.ToLowerInvariant()}", false, true); if (type != null) { int count = item.Components.Count(ic => ic.GetType() == type); if (count == 0) { return null; } IEnumerable itemComponents = item.Components.Where(ic => ic.GetType() == type); return itemComponents.Cast().ToArray(); } return null; } public void Save(XElement element) { var upgrade = new XElement("Upgrade", new XAttribute("identifier", Identifier), new XAttribute("level", Level)); foreach (var targetComponent in TargetComponents) { var (key, value) = targetComponent; string name = key is ItemComponent ? key.Name : "This"; var subElement = new XElement(name); foreach (PropertyReference propertyRef in value) { if (propertyRef.OriginalValue != null) { subElement.Add(new XElement(propertyRef.Name.Value, new XAttribute("value", propertyRef.OriginalValue))); } else if (!Prefab.SuppressWarnings) { DebugConsole.AddWarning($"Failed to save upgrade \"{Prefab.Name}\" on {TargetEntity.Name} because property reference \"{propertyRef.Name}\" is missing original values. \n" + "Upgrades should always call Upgrade.ApplyUpgrade() or manually set the original value in a property reference after they have been added. \n" + "If you are not a developer submit a bug report at https://github.com/Regalis11/Barotrauma/issues/.", Prefab.ContentPackage); } } upgrade.Add(subElement); } element.Add(upgrade); } /// /// Applies the upgrade to the target item and components /// /// /// This method should be called every time a new upgrade is added unless you set the original values of PropertyReference manually. /// Do note that calls this method automatically. /// public void ApplyUpgrade() { foreach (var keyValuePair in TargetComponents) { var (entity, properties) = keyValuePair; foreach (PropertyReference propertyReference in properties) { if (entity.SerializableProperties.TryGetValue(propertyReference.Name, out SerializableProperty? property) && property != null) { object? originalValue = property.GetValue(entity); propertyReference.SetOriginalValue(originalValue); object newValue = Convert.ChangeType(propertyReference.CalculateUpgrade(Level), originalValue.GetType(), NumberFormatInfo.InvariantInfo); property.SetValue(entity, newValue); } else { // Find the closest matching property name and suggest it in the error message string matchingString = string.Empty; int closestMatch = int.MaxValue; foreach (var (propertyName, _) in entity.SerializableProperties) { int match = ToolBox.LevenshteinDistance(propertyName.Value, propertyReference.Name.Value); if (match < closestMatch) { matchingString = propertyName.Value ?? ""; closestMatch = match; } } DebugConsole.ThrowError($"The upgrade \"{Prefab.Name}\" cannot be applied to {entity.Name} because it does not contain the property \"{propertyReference.Name}\" and has been ignored. \n" + $"Did you mean \"{matchingString}\"?"); } } } } public void Dispose() { if (!Disposed) { TargetComponents.Clear(); } Disposed = true; } } }