3 using System.Collections.Generic;
4 using System.Globalization;
12 internal sealed
class PropertyReference
14 public object? OriginalValue {
get;
private set; }
16 public readonly Identifier Name;
18 private readonly
string Multiplier;
20 private static readonly
char[] prefixCharacters = {
'=',
'/',
'*',
'x',
'-',
'+' };
22 private readonly Upgrade upgrade;
24 private PropertyReference(Identifier name,
string multiplier, Upgrade upgrade)
27 this.Multiplier = multiplier;
28 this.upgrade = upgrade;
31 public void SetOriginalValue(
object value)
33 OriginalValue ??= value;
41 public object CalculateUpgrade(
int level)
43 switch (OriginalValue)
49 var value = Convert.ToSingle(OriginalValue);
50 return level == 0 ? value : CalculateUpgrade(value, level, Multiplier);
52 case bool _ when
bool.TryParse(Multiplier, out
bool result):
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()?");
67 public static float CalculateUpgrade(
float value,
int level,
string multiplier)
69 if (multiplier[^1] !=
'%')
71 return CalculateUpgradeFloat(multiplier, value , level);
74 return ApplyPercentage(value, UpgradePrefab.ParsePercentage(multiplier, Identifier.Empty, suppressWarnings:
true), level);
77 private static float CalculateUpgradeFloat(
string multiplier,
float value,
int level)
79 float multiplierFloat = ParseValue(multiplier, value);
81 switch (multiplier[0])
85 return value * (multiplierFloat * level);
87 return value / (multiplierFloat * level);
89 return value - (multiplierFloat * level);
91 return value + (multiplierFloat * level);
93 return multiplierFloat;
103 public void ApplySavedValue(XElement? savedElement)
105 if (savedElement ==
null) {
return; }
107 foreach (var savedValue
in savedElement.Elements())
109 if (savedValue.NameAsIdentifier() == Name)
111 string value = savedValue.GetAttributeString(
"value",
string.Empty);
113 if (
float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out
float floatValue))
115 OriginalValue = floatValue;
117 else if (
bool.TryParse(value, out
bool boolValue))
119 OriginalValue = boolValue;
123 OriginalValue = value;
136 private static float ApplyPercentage(
float value,
float amount,
int times)
138 return (1f + (amount / 100f * times)) * value;
141 public static PropertyReference[] ParseAttributes(IEnumerable<XAttribute> attributes, Upgrade upgrade)
143 return attributes.Select(attribute =>
new PropertyReference(attribute.NameAsIdentifier(), attribute.Value, upgrade)).ToArray();
146 private static float ParseValue(
string multiplier,
object? originalValue)
148 if (multiplier.Length > 1)
150 if (prefixCharacters.Contains(multiplier[0]))
152 if (
float.TryParse(multiplier.Substring(1).Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out
float value)) {
return value; }
154 if (originalValue is
float || originalValue is
int || originalValue is
double) {
return (
float) originalValue; }
162 internal sealed
class Upgrade : IDisposable
164 private ISerializableEntity TargetEntity {
get; }
166 public Dictionary<ISerializableEntity, PropertyReference[]> TargetComponents {
get; }
168 public UpgradePrefab Prefab {
get; }
170 public Identifier Identifier => Prefab.Identifier;
172 public int Level {
get;
set; }
174 public bool Disposed {
get;
private set; }
176 private readonly ContentXElement sourceElement;
178 public Upgrade(ISerializableEntity targetEntity, UpgradePrefab prefab,
int level, XContainer? saveElement =
null)
180 this.TargetEntity = targetEntity;
181 this.sourceElement = prefab.SourceElement;
182 this.Prefab = prefab;
185 var targetProperties =
new Dictionary<ISerializableEntity, PropertyReference[]>();
187 List<XElement>? saveElements = saveElement?.Elements().ToList();
189 foreach (var subElement
in prefab.SourceElement.Elements())
191 switch (subElement.Name.ToString().ToLowerInvariant())
193 case "decorativesprite":
202 XElement? savedRootElement = saveElements?.Find(e =>
string.Equals(e.Name.ToString(),
"This", StringComparison.OrdinalIgnoreCase));
204 var rootProperties = PropertyReference.ParseAttributes(subElement.Attributes(),
this);
205 targetProperties.Add(targetEntity, rootProperties);
207 foreach (var propertyRef
in rootProperties)
209 propertyRef.ApplySavedValue(savedRootElement);
215 if (targetEntity is Item item)
217 ISerializableEntity[]? itemComponents = FindItemComponent(item, subElement.Name.ToString());
219 if (itemComponents !=
null && itemComponents.Any())
221 foreach (ISerializableEntity sEntity
in itemComponents)
223 XElement? savedElement = saveElements?.Find(e =>
string.Equals(e.Name.ToString(), sEntity.Name, StringComparison.OrdinalIgnoreCase));
224 PropertyReference[] properties = PropertyReference.ParseAttributes(subElement.Attributes(),
this);
226 foreach (PropertyReference propertyRef
in properties)
228 propertyRef.ApplySavedValue(savedElement);
231 targetProperties.Add(sEntity, properties);
241 TargetComponents = targetProperties;
243 if (saveElement !=
null)
245 ResetNonAffectedProperties(saveElement);
253 private void ResetNonAffectedProperties(XContainer saveElement)
255 foreach (var element
in saveElement.Elements().Elements())
257 if (TargetComponents.SelectMany(pair => pair.Value)
258 .Select(@ref => @ref.Name)
259 .Any(@identifier => @identifier == element.NameAsIdentifier())) {
continue; }
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();
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.");
269 if (
string.Equals(componentName,
"This", StringComparison.OrdinalIgnoreCase))
271 if (TargetEntity.SerializableProperties.TryGetValue(name, out SerializableProperty? property))
273 property?.SetValue(TargetEntity, Convert.ChangeType(value, property!.GetValue(TargetEntity).GetType(), NumberFormatInfo.InvariantInfo));
276 else if (TargetEntity is Item item)
278 ISerializableEntity[]? foundComponents = FindItemComponent(item, componentName);
279 if (foundComponents ==
null) {
continue; }
281 foreach (var serializableEntity
in foundComponents)
283 if (serializableEntity.SerializableProperties.TryGetValue(name, out SerializableProperty? property))
285 property?.SetValue(serializableEntity, Convert.ChangeType(value, property!.GetValue(serializableEntity).GetType(), NumberFormatInfo.InvariantInfo));
298 private static ISerializableEntity[]? FindItemComponent(Item item,
string name)
300 Type? type = Type.GetType($
"Barotrauma.Items.Components.{name.ToLowerInvariant()}",
false,
true);
304 int count = item.Components.Count(ic => ic.GetType() == type);
305 if (count == 0) {
return null; }
307 IEnumerable<ItemComponent> itemComponents = item.Components.Where(ic => ic.GetType() == type);
308 return itemComponents.Cast<ISerializableEntity>().ToArray();
314 public void Save(XElement element)
316 var upgrade =
new XElement(
"Upgrade",
new XAttribute(
"identifier", Identifier),
new XAttribute(
"level", Level));
318 foreach (var targetComponent
in TargetComponents)
320 var (key, value) = targetComponent;
324 var subElement =
new XElement(name);
325 foreach (PropertyReference propertyRef
in value)
327 if (propertyRef.OriginalValue !=
null)
329 subElement.Add(
new XElement(propertyRef.Name.Value,
330 new XAttribute(
"value", propertyRef.OriginalValue)));
332 else if (!Prefab.SuppressWarnings)
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);
341 upgrade.Add(subElement);
344 element.Add(upgrade);
354 public void ApplyUpgrade()
356 foreach (var keyValuePair
in TargetComponents)
358 var (entity, properties) = keyValuePair;
360 foreach (PropertyReference propertyReference
in properties)
362 if (entity.SerializableProperties.TryGetValue(propertyReference.Name, out SerializableProperty? property) && property !=
null)
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);
372 string matchingString =
string.Empty;
373 int closestMatch =
int.MaxValue;
374 foreach (var (propertyName, _) in entity.SerializableProperties)
376 int match = ToolBox.LevenshteinDistance(propertyName.Value, propertyReference.Name.Value);
377 if (match < closestMatch)
379 matchingString = propertyName.Value ??
"";
380 closestMatch = match;
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}\"?");
391 public void Dispose()
395 TargetComponents.Clear();
The base class for components holding the different functionalities of the item