Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs
2023-11-10 17:45:19 +02:00

401 lines
16 KiB
C#

#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;
}
/// <summary>
/// Calculate the new value of the property
/// </summary>
/// <param name="level">level of the upgrade</param>
/// <returns></returns>
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;
}
/// <summary>
/// Sets the OriginalValue to a value stored in the save XML element
/// </summary>
/// <param name="savedElement"></param>
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;
}
}
}
}
/// <summary>
/// Recursively apply a percentage to a value certain amount of times
/// </summary>
/// <param name="value">original value</param>
/// <param name="amount">percentage increase/decrease</param>
/// <param name="times">how many times to apply the percentage change</param>
/// <returns></returns>
private static float ApplyPercentage(float value, float amount, int times)
{
return (1f + (amount / 100f * times)) * value;
}
public static PropertyReference[] ParseAttributes(IEnumerable<XAttribute> 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<ISerializableEntity, PropertyReference[]> 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<ISerializableEntity, PropertyReference[]>();
List<XElement>? 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);
}
}
/// <summary>
/// Finds saved properties in the XML element and resets properties that are not managed by the upgrade anymore to their default values
/// </summary>
/// <param name="saveElement">XML save element</param>
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));
}
}
}
}
}
/// <summary>
/// Find an item component matching the XML element
/// </summary>
/// <param name="item">Target item</param>
/// <param name="name">XML ItemComponent element</param>
/// <returns>Array of matching ItemComponents or null</returns>
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<ItemComponent> itemComponents = item.Components.Where(ic => ic.GetType() == type);
return itemComponents.Cast<ISerializableEntity>().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);
}
/// <summary>
/// Applies the upgrade to the target item and components
/// </summary>
/// <remarks>
/// This method should be called every time a new upgrade is added unless you set the original values of PropertyReference manually.
/// Do note that <see cref="MapEntity.AddUpgrade"/> calls this method automatically.
/// </remarks>
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;
}
}
}