Client LuaCsForBarotrauma
Upgrade.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Linq;
6 using System.Xml.Linq;
8 
9 // ReSharper disable ArrangeThisQualifier
10 namespace Barotrauma
11 {
12  internal sealed class PropertyReference
13  {
14  public object? OriginalValue { get; private set; }
15 
16  public readonly Identifier Name;
17 
18  private readonly string Multiplier;
19 
20  private static readonly char[] prefixCharacters = { '=', '/', '*', 'x', '-', '+' };
21 
22  private readonly Upgrade upgrade;
23 
24  private PropertyReference(Identifier name, string multiplier, Upgrade upgrade)
25  {
26  this.Name = name;
27  this.Multiplier = multiplier;
28  this.upgrade = upgrade;
29  }
30 
31  public void SetOriginalValue(object value)
32  {
33  OriginalValue ??= value;
34  }
35 
41  public object CalculateUpgrade(int level)
42  {
43  switch (OriginalValue)
44  {
45  case float _:
46  case int _:
47  case double _:
48  {
49  var value = Convert.ToSingle(OriginalValue);
50  return level == 0 ? value : CalculateUpgrade(value, level, Multiplier);
51  }
52  case bool _ when bool.TryParse(Multiplier, out bool result):
53  {
54  return result;
55  }
56  default:
57  {
58  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" +
59  "The value has been assumed to be '0', did you forget a Convert.ChangeType()?");
60  break;
61  }
62  }
63 
64  return 0;
65  }
66 
67  public static float CalculateUpgrade(float value, int level, string multiplier)
68  {
69  if (multiplier[^1] != '%')
70  {
71  return CalculateUpgradeFloat(multiplier, value , level);
72  }
73 
74  return ApplyPercentage(value, UpgradePrefab.ParsePercentage(multiplier, Identifier.Empty, suppressWarnings: true), level);
75  }
76 
77  private static float CalculateUpgradeFloat(string multiplier, float value, int level)
78  {
79  float multiplierFloat = ParseValue(multiplier, value);
80 
81  switch (multiplier[0])
82  {
83  case '*':
84  case 'x':
85  return value * (multiplierFloat * level);
86  case '/':
87  return value / (multiplierFloat * level);
88  case '-':
89  return value - (multiplierFloat * level);
90  case '+':
91  return value + (multiplierFloat * level);
92  case '=':
93  return multiplierFloat;
94  }
95 
96  return 0;
97  }
98 
103  public void ApplySavedValue(XElement? savedElement)
104  {
105  if (savedElement == null) { return; }
106 
107  foreach (var savedValue in savedElement.Elements())
108  {
109  if (savedValue.NameAsIdentifier() == Name)
110  {
111  string value = savedValue.GetAttributeString("value", string.Empty);
112 
113  if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out float floatValue))
114  {
115  OriginalValue = floatValue;
116  }
117  else if (bool.TryParse(value, out bool boolValue))
118  {
119  OriginalValue = boolValue;
120  }
121  else
122  {
123  OriginalValue = value;
124  }
125  }
126  }
127  }
128 
136  private static float ApplyPercentage(float value, float amount, int times)
137  {
138  return (1f + (amount / 100f * times)) * value;
139  }
140 
141  public static PropertyReference[] ParseAttributes(IEnumerable<XAttribute> attributes, Upgrade upgrade)
142  {
143  return attributes.Select(attribute => new PropertyReference(attribute.NameAsIdentifier(), attribute.Value, upgrade)).ToArray();
144  }
145 
146  private static float ParseValue(string multiplier, object? originalValue)
147  {
148  if (multiplier.Length > 1)
149  {
150  if (prefixCharacters.Contains(multiplier[0]))
151  {
152  if (float.TryParse(multiplier.Substring(1).Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out float value)) { return value; }
153 
154  if (originalValue is float || originalValue is int || originalValue is double) { return (float) originalValue; }
155  }
156  }
157 
158  return 1;
159  }
160  }
161 
162  internal sealed class Upgrade : IDisposable
163  {
164  private ISerializableEntity TargetEntity { get; }
165 
166  public Dictionary<ISerializableEntity, PropertyReference[]> TargetComponents { get; }
167 
168  public UpgradePrefab Prefab { get; }
169 
170  public Identifier Identifier => Prefab.Identifier;
171 
172  public int Level { get; set; }
173 
174  public bool Disposed { get; private set; }
175 
176  private readonly ContentXElement sourceElement;
177 
178  public Upgrade(ISerializableEntity targetEntity, UpgradePrefab prefab, int level, XContainer? saveElement = null)
179  {
180  this.TargetEntity = targetEntity;
181  this.sourceElement = prefab.SourceElement;
182  this.Prefab = prefab;
183  this.Level = level;
184 
185  var targetProperties = new Dictionary<ISerializableEntity, PropertyReference[]>();
186 
187  List<XElement>? saveElements = saveElement?.Elements().ToList();
188 
189  foreach (var subElement in prefab.SourceElement.Elements())
190  {
191  switch (subElement.Name.ToString().ToLowerInvariant())
192  {
193  case "decorativesprite":
194  case "sprite":
195  case "price":
196  break;
197  case "item":
198  case "structure":
199  case "base":
200  case "root":
201  case "this":
202  XElement? savedRootElement = saveElements?.Find(e => string.Equals(e.Name.ToString(), "This", StringComparison.OrdinalIgnoreCase));
203 
204  var rootProperties = PropertyReference.ParseAttributes(subElement.Attributes(), this);
205  targetProperties.Add(targetEntity, rootProperties);
206 
207  foreach (var propertyRef in rootProperties)
208  {
209  propertyRef.ApplySavedValue(savedRootElement);
210  }
211 
212  break;
213  default:
214  {
215  if (targetEntity is Item item)
216  {
217  ISerializableEntity[]? itemComponents = FindItemComponent(item, subElement.Name.ToString());
218 
219  if (itemComponents != null && itemComponents.Any())
220  {
221  foreach (ISerializableEntity sEntity in itemComponents)
222  {
223  XElement? savedElement = saveElements?.Find(e => string.Equals(e.Name.ToString(), sEntity.Name, StringComparison.OrdinalIgnoreCase));
224  PropertyReference[] properties = PropertyReference.ParseAttributes(subElement.Attributes(), this);
225 
226  foreach (PropertyReference propertyRef in properties)
227  {
228  propertyRef.ApplySavedValue(savedElement);
229  }
230 
231  targetProperties.Add(sEntity, properties);
232  }
233  }
234  }
235 
236  break;
237  }
238  }
239  }
240 
241  TargetComponents = targetProperties;
242 
243  if (saveElement != null)
244  {
245  ResetNonAffectedProperties(saveElement);
246  }
247  }
248 
253  private void ResetNonAffectedProperties(XContainer saveElement)
254  {
255  foreach (var element in saveElement.Elements().Elements())
256  {
257  if (TargetComponents.SelectMany(pair => pair.Value)
258  .Select(@ref => @ref.Name)
259  .Any(@identifier => @identifier == element.NameAsIdentifier())) { continue; }
260 
261  string value = element.GetAttributeString("value", string.Empty);
262  Identifier name = element.NameAsIdentifier();
263  XElement parentElement = element.Parent ?? throw new NullReferenceException("Unable to reset properties: Parent element is null.");
264  string componentName = parentElement.Name.ToString();
265 
266  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" +
267  $"The property has been reset to the original value of {value} and will be ignored from now on.");
268 
269  if (string.Equals(componentName, "This", StringComparison.OrdinalIgnoreCase))
270  {
271  if (TargetEntity.SerializableProperties.TryGetValue(name, out SerializableProperty? property))
272  {
273  property?.SetValue(TargetEntity, Convert.ChangeType(value, property!.GetValue(TargetEntity).GetType(), NumberFormatInfo.InvariantInfo));
274  }
275  }
276  else if (TargetEntity is Item item)
277  {
278  ISerializableEntity[]? foundComponents = FindItemComponent(item, componentName);
279  if (foundComponents == null) { continue; }
280 
281  foreach (var serializableEntity in foundComponents)
282  {
283  if (serializableEntity.SerializableProperties.TryGetValue(name, out SerializableProperty? property))
284  {
285  property?.SetValue(serializableEntity, Convert.ChangeType(value, property!.GetValue(serializableEntity).GetType(), NumberFormatInfo.InvariantInfo));
286  }
287  }
288  }
289  }
290  }
291 
298  private static ISerializableEntity[]? FindItemComponent(Item item, string name)
299  {
300  Type? type = Type.GetType($"Barotrauma.Items.Components.{name.ToLowerInvariant()}", false, true);
301 
302  if (type != null)
303  {
304  int count = item.Components.Count(ic => ic.GetType() == type);
305  if (count == 0) { return null; }
306 
307  IEnumerable<ItemComponent> itemComponents = item.Components.Where(ic => ic.GetType() == type);
308  return itemComponents.Cast<ISerializableEntity>().ToArray();
309  }
310 
311  return null;
312  }
313 
314  public void Save(XElement element)
315  {
316  var upgrade = new XElement("Upgrade", new XAttribute("identifier", Identifier), new XAttribute("level", Level));
317 
318  foreach (var targetComponent in TargetComponents)
319  {
320  var (key, value) = targetComponent;
321 
322  string name = key is ItemComponent ? key.Name : "This";
323 
324  var subElement = new XElement(name);
325  foreach (PropertyReference propertyRef in value)
326  {
327  if (propertyRef.OriginalValue != null)
328  {
329  subElement.Add(new XElement(propertyRef.Name.Value,
330  new XAttribute("value", propertyRef.OriginalValue)));
331  }
332  else if (!Prefab.SuppressWarnings)
333  {
334  DebugConsole.AddWarning($"Failed to save upgrade \"{Prefab.Name}\" on {TargetEntity.Name} because property reference \"{propertyRef.Name}\" is missing original values. \n" +
335  "Upgrades should always call Upgrade.ApplyUpgrade() or manually set the original value in a property reference after they have been added. \n" +
336  "If you are not a developer submit a bug report at https://github.com/Regalis11/Barotrauma/issues/.",
337  Prefab.ContentPackage);
338  }
339  }
340 
341  upgrade.Add(subElement);
342  }
343 
344  element.Add(upgrade);
345  }
346 
354  public void ApplyUpgrade()
355  {
356  foreach (var keyValuePair in TargetComponents)
357  {
358  var (entity, properties) = keyValuePair;
359 
360  foreach (PropertyReference propertyReference in properties)
361  {
362  if (entity.SerializableProperties.TryGetValue(propertyReference.Name, out SerializableProperty? property) && property != null)
363  {
364  object? originalValue = property.GetValue(entity);
365  propertyReference.SetOriginalValue(originalValue);
366  object newValue = Convert.ChangeType(propertyReference.CalculateUpgrade(Level), originalValue.GetType(), NumberFormatInfo.InvariantInfo);
367  property.SetValue(entity, newValue);
368  }
369  else
370  {
371  // Find the closest matching property name and suggest it in the error message
372  string matchingString = string.Empty;
373  int closestMatch = int.MaxValue;
374  foreach (var (propertyName, _) in entity.SerializableProperties)
375  {
376  int match = ToolBox.LevenshteinDistance(propertyName.Value, propertyReference.Name.Value);
377  if (match < closestMatch)
378  {
379  matchingString = propertyName.Value ?? "";
380  closestMatch = match;
381  }
382  }
383 
384  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" +
385  $"Did you mean \"{matchingString}\"?");
386  }
387  }
388  }
389  }
390 
391  public void Dispose()
392  {
393  if (!Disposed)
394  {
395  TargetComponents.Clear();
396  }
397 
398  Disposed = true;
399  }
400  }
401 }
The base class for components holding the different functionalities of the item