Server LuaCsForBarotrauma
UpgradePrefab.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Collections.Immutable;
5 using System.Globalization;
6 using System.Linq;
7 using System.Xml.Linq;
9 using Microsoft.Xna.Framework;
10 
11 namespace Barotrauma
12 {
13  internal readonly struct UpgradePrice
14  {
15  public readonly int BasePrice;
16 
17  public readonly int IncreaseLow;
18 
19  public readonly int IncreaseHigh;
20 
21  public UpgradePrice(UpgradePrefab prefab, ContentXElement element)
22  {
23  IncreaseLow = UpgradePrefab.ParsePercentage(element.GetAttributeString("increaselow", string.Empty)!,
24  "IncreaseLow".ToIdentifier(), element, suppressWarnings: prefab.SuppressWarnings);
25 
26  IncreaseHigh = UpgradePrefab.ParsePercentage(element.GetAttributeString("increasehigh", string.Empty)!,
27  "IncreaseHigh".ToIdentifier(), element, suppressWarnings: prefab.SuppressWarnings);
28 
29  BasePrice = element.GetAttributeInt("baseprice", -1);
30 
31  if (BasePrice == -1)
32  {
33  if (!prefab.SuppressWarnings)
34  {
35  DebugConsole.AddWarning($"Price attribute \"baseprice\" is not defined for {prefab?.Identifier}.\n " +
36  "The value has been assumed to be '1000'.",
37  prefab!.ContentPackage);
38  BasePrice = 1000;
39  }
40  }
41  }
42 
43  public int GetBuyPrice(UpgradePrefab prefab, int level, Location? location = null, ImmutableHashSet<Character>? characterList = null)
44  {
45  float price = BasePrice;
46 
47  int maxLevel = prefab.MaxLevel;
48 
49  float lerpAmount = maxLevel is 0
50  ? level // avoid division by 0
51  : level / (float)maxLevel;
52 
53  float priceMultiplier = MathHelper.Lerp(IncreaseLow, IncreaseHigh, lerpAmount);
54  price += price * (priceMultiplier / 100f);
55 
56  price = location?.GetAdjustedMechanicalCost((int)price) ?? price;
57 
58  // Adjust by campaign difficulty settings
59  if (GameMain.GameSession?.Campaign is CampaignMode campaign)
60  {
61  price *= campaign.Settings.ShipyardPriceMultiplier;
62  }
63 
64  characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both);
65 
66  if (characterList.Any())
67  {
68  if (location?.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
69  {
70  price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated));
71  }
72  price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier));
73  }
74  return (int)price;
75  }
76  }
77 
78  abstract class UpgradeContentPrefab : Prefab
79  {
81  onAdd: (prefab, isOverride) =>
82  {
83  if (prefab is UpgradePrefab upgradePrefab)
84  {
85  UpgradePrefab.Prefabs.Add(upgradePrefab, isOverride);
86  }
87  else if (prefab is UpgradeCategory upgradeCategory)
88  {
89  UpgradeCategory.Categories.Add(upgradeCategory, isOverride);
90  }
91  },
92  onRemove: (prefab) =>
93  {
94  if (prefab is UpgradePrefab upgradePrefab)
95  {
96  UpgradePrefab.Prefabs.Remove(upgradePrefab);
97  }
98  else if (prefab is UpgradeCategory upgradeCategory)
99  {
100  UpgradeCategory.Categories.Remove(upgradeCategory);
101  }
102  },
103  onSort: () =>
104  {
105  UpgradePrefab.Prefabs.SortAll();
106  UpgradeCategory.Categories.SortAll();
107  },
108  onAddOverrideFile: (file) =>
109  {
110  UpgradePrefab.Prefabs.AddOverrideFile(file);
111  UpgradeCategory.Categories.AddOverrideFile(file);
112  },
113  onRemoveOverrideFile: (file) =>
114  {
115  UpgradePrefab.Prefabs.RemoveOverrideFile(file);
116  UpgradeCategory.Categories.RemoveOverrideFile(file);
117  });
118 
119  public UpgradeContentPrefab(ContentXElement element, UpgradeModulesFile file) : base(file, element) { }
120  }
121 
122  internal class UpgradeCategory : UpgradeContentPrefab
123  {
124  public static readonly PrefabCollection<UpgradeCategory> Categories = new PrefabCollection<UpgradeCategory>();
125 
126  private readonly ImmutableHashSet<Identifier> selfItemTags;
127  private readonly HashSet<Identifier> prefabsThatAllowUpgrades = new HashSet<Identifier>();
128  public readonly bool IsWallUpgrade;
129  public readonly LocalizedString Name;
130 
131  private readonly object mutex = new object();
132 
133  public readonly IEnumerable<Identifier> ItemTags;
134 
135  public UpgradeCategory(ContentXElement element, UpgradeModulesFile file) : base(element, file)
136  {
137  selfItemTags = element.GetAttributeIdentifierArray("items", Array.Empty<Identifier>())?.ToImmutableHashSet() ?? ImmutableHashSet<Identifier>.Empty;
138  Name = element.GetAttributeString("name", string.Empty)!;
139  IsWallUpgrade = element.GetAttributeBool("wallupgrade", false);
140 
141  ItemTags = selfItemTags.CollectionConcat(prefabsThatAllowUpgrades);
142 
143  Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
144  if (!nameIdentifier.IsEmpty)
145  {
146  Name = TextManager.Get($"{nameIdentifier}");
147  }
148  else if (Name.IsNullOrWhiteSpace())
149  {
150  Name = TextManager.Get($"UpgradeCategory.{Identifier}");
151  }
152  }
153 
154  public void DeterminePrefabsThatAllowUpgrades()
155  {
156  lock (mutex)
157  {
158  prefabsThatAllowUpgrades.Clear();
159  prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs
160  .Where(it => it.GetAllowedUpgrades().Contains(Identifier))
161  .Select(it => it.Identifier));
162  }
163  }
164 
165  public bool CanBeApplied(MapEntity item, UpgradePrefab? upgradePrefab)
166  {
167  if (upgradePrefab != null && item.Submarine is { Info: var info } && !upgradePrefab.IsApplicable(info)) { return false; }
168 
169  bool isStructure = item is Structure;
170  switch (IsWallUpgrade)
171  {
172  case true:
173  return isStructure;
174  case false when isStructure:
175  return false;
176  }
177 
178  if (upgradePrefab != null && upgradePrefab.IsDisallowed(item)) { return false; }
179 
180  lock (mutex)
181  {
182  return item.Prefab.GetAllowedUpgrades().Contains(Identifier) ||
183  ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag);
184  }
185  }
186 
187  public static UpgradeCategory? Find(Identifier identifier)
188  {
189  return !identifier.IsEmpty ? Categories.Find(category => category.Identifier == identifier) : null;
190  }
191 
192  public override void Dispose() { }
193  }
194 
195  internal readonly struct UpgradeMaxLevelMod
196  {
197  private enum MaxLevelModType
198  {
199  Invalid,
200  Increase,
201  Set
202  }
203 
204  private readonly Either<SubmarineClass, int> tierOrClass;
205  private readonly int value;
206  private readonly MaxLevelModType type;
207 
208  public int GetLevelAfter(int level) =>
209  type switch
210  {
211  MaxLevelModType.Invalid => level,
212  MaxLevelModType.Increase => level + value,
213  MaxLevelModType.Set => value,
214  _ => throw new ArgumentOutOfRangeException()
215  };
216 
217  public bool AppliesTo(SubmarineClass subClass, int subTier)
218  {
219  if (type is MaxLevelModType.Invalid) { return false; }
220 
221  if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata)
222  {
223  int modifier = metadata.GetInt(new Identifier("tiermodifieroverride"), 0);
224 
225  subTier = Math.Max(modifier, subTier);
226  }
227 
228  if (tierOrClass.TryGet(out int tier))
229  {
230  return subTier == tier;
231  }
232 
233  if (tierOrClass.TryGet(out SubmarineClass targetClass))
234  {
235  return subClass == targetClass;
236  }
237 
238  return false;
239  }
240 
241  public UpgradeMaxLevelMod(ContentXElement element)
242  {
243  bool isValid = true;
244 
245  SubmarineClass subClass = element.GetAttributeEnum("class", SubmarineClass.Undefined);
246  int tier = element.GetAttributeInt("tier", 0);
247  if (subClass != SubmarineClass.Undefined)
248  {
249  tierOrClass = subClass;
250  }
251  else
252  {
253  tierOrClass = tier;
254  }
255 
256  string stringValue = element.GetAttributeString("level", null) ?? string.Empty;
257  value = 0;
258 
259  if (string.IsNullOrWhiteSpace(stringValue)) { isValid = false; }
260 
261  char firstChar = stringValue[0];
262 
263  if (!int.TryParse(stringValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var intValue)) { isValid = false; }
264  value = intValue;
265 
266  if (firstChar.Equals('+') || firstChar.Equals('-'))
267  {
268  type = MaxLevelModType.Increase;
269  }
270  else
271  {
272  type = MaxLevelModType.Set;
273  }
274 
275  if (!isValid) { type = MaxLevelModType.Invalid; }
276  }
277  }
278 
279  internal readonly struct UpgradeResourceCost
280  {
281  public readonly int Amount;
282  private readonly ImmutableArray<Identifier> targetTags;
283  public readonly Range<int> TargetLevels;
284 
285  public UpgradeResourceCost(ContentXElement element)
286  {
287  Amount = element.GetAttributeInt("amount", 0);
288  targetTags = element.GetAttributeIdentifierArray("item", Array.Empty<Identifier>())!.ToImmutableArray();
289  TargetLevels = element.GetAttributeRange("levels", new Range<int>(0, 99));
290  }
291 
292  public bool AppliesForLevel(int currentLevel) => TargetLevels.Contains(currentLevel);
293 
294  public bool AppliesForLevel(Range<int> newLevels) => newLevels.Start <= TargetLevels.End && newLevels.End >= TargetLevels.Start;
295 
296  public bool MatchesItem(Item item) => MatchesItem(item.Prefab);
297 
298  public bool MatchesItem(ItemPrefab item)
299  {
300  foreach (Identifier tag in targetTags)
301  {
302  if (tag.Equals(item.Identifier) || item.Tags.Contains(tag)) { return true; }
303  }
304 
305  return false;
306  }
307  }
308 
309  internal readonly struct ApplicableResourceCollection
310  {
311  public readonly ImmutableArray<ItemPrefab> MatchingItems;
312  public readonly UpgradeResourceCost Cost;
313  public readonly int Count;
314 
315  public ApplicableResourceCollection(IEnumerable<ItemPrefab> matchingItems, int count, UpgradeResourceCost cost)
316  {
317  MatchingItems = matchingItems.ToImmutableArray();
318  Count = count;
319  Cost = cost;
320  }
321 
322  public static ApplicableResourceCollection CreateFor(UpgradeResourceCost cost)
323  {
324  return new ApplicableResourceCollection(ItemPrefab.Prefabs.Where(cost.MatchesItem), cost.Amount, cost);
325  }
326  }
327 
328  internal sealed partial class UpgradePrefab : UpgradeContentPrefab
329  {
330  public static readonly PrefabCollection<UpgradePrefab> Prefabs = new PrefabCollection<UpgradePrefab>(
331  onAdd: static (prefab, isOverride) =>
332  {
333  if (!prefab.SuppressWarnings && !isOverride)
334  {
335  foreach (UpgradePrefab matchingPrefab in Prefabs?.Where(p => p != prefab && p.TargetItems.Any(s => prefab.TargetItems.Contains(s))) ?? throw new NullReferenceException("Honestly I have no clue why this could be null..."))
336  {
337  if (matchingPrefab.isOverride) { continue; }
338 
339  var upgradePrefab = matchingPrefab.targetProperties;
340  string key = string.Empty;
341 
342  if (upgradePrefab.Keys.Any(s => prefab.targetProperties.Keys.Any(s1 => s == (key = s1))))
343  {
344  if (upgradePrefab.ContainsKey(key) && upgradePrefab[key].Any(s => prefab.targetProperties[key].Contains(s)))
345  {
346  DebugConsole.AddWarning($"Upgrade \"{prefab.Identifier}\" is affecting a property that is also being affected by \"{matchingPrefab.Identifier}\".\n" +
347  "This is unsupported and might yield unexpected results if both upgrades are applied at the same time to the same item.\n" +
348  "Add the attribute suppresswarnings=\"true\" to your XML element to disable this warning if you know what you're doing.",
349  prefab.ContentPackage);
350  }
351  }
352  }
353  }
354  },
355  onRemove: null,
356  onSort: null,
357  onAddOverrideFile: null,
358  onRemoveOverrideFile: null
359  );
360 
364  public readonly int MaxLevel;
365 
366  public LocalizedString Name { get; }
367 
368  public LocalizedString Description { get; }
369 
370  public float IncreaseOnTooltip { get; }
371 
372  private readonly ImmutableHashSet<Identifier> upgradeCategoryIdentifiers;
373 
374  public IEnumerable<UpgradeCategory> UpgradeCategories
375  {
376  get
377  {
378  foreach (var id in upgradeCategoryIdentifiers)
379  {
380  if (UpgradeCategory.Categories.TryGet(id, out var category)) { yield return category!; }
381  }
382  }
383  }
384 
385  public UpgradePrice Price { get; }
386 
387  private bool isOverride => Prefabs.IsOverride(this);
388 
389  public ContentXElement SourceElement { get; }
390 
391  public bool SuppressWarnings { get; }
392 
393  public bool HideInMenus { get; }
394 
395  public IEnumerable<Identifier> TargetItems => UpgradeCategories.SelectMany(u => u.ItemTags);
396 
397  public bool IsWallUpgrade => UpgradeCategories.All(u => u.IsWallUpgrade);
398 
399  private Dictionary<string, string[]> targetProperties { get; }
400  private readonly ImmutableArray<UpgradeMaxLevelMod> MaxLevelsMods;
401  public readonly ImmutableHashSet<UpgradeResourceCost> ResourceCosts;
402 
403  public const int CrushDepthDefaultUpgradePrc = 15;
404  private static int? crushDepthUpgradePrc;
405 
406  public static int CrushDepthUpgradePrc
407  {
408  get
409  {
410  if (crushDepthUpgradePrc == null)
411  {
412  if (Find("increasewallhealth".ToIdentifier()) is UpgradePrefab hullUpgradePrefab)
413  {
414  string updateValueStr = hullUpgradePrefab.SourceElement?.GetChildElement("Structure")?.GetAttributeString("crushdepth", null) ?? string.Empty;
415  if (!string.IsNullOrEmpty(updateValueStr))
416  {
417  crushDepthUpgradePrc = ParsePercentage(updateValueStr, Identifier.Empty, suppressWarnings: true);
418  }
419  }
420  }
421 
422  return crushDepthUpgradePrc ?? CrushDepthDefaultUpgradePrc;
423  }
424  }
425 
426  public const int IncreaseWallHealthDefaultMaxLevel = 6;
427  private static int? increaseWallHealthMaxLevel;
428 
429  public static int IncreaseWallHealthMaxLevel
430  {
431  get
432  {
433  if (increaseWallHealthMaxLevel == null)
434  {
435  if (Find("increasewallhealth".ToIdentifier()) is UpgradePrefab hullUpgradePrefab)
436  {
437  increaseWallHealthMaxLevel = hullUpgradePrefab.MaxLevel;
438  }
439  }
440 
441  return increaseWallHealthMaxLevel ?? IncreaseWallHealthDefaultMaxLevel;
442  }
443  }
444 
445  public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file)
446  {
447  Name = element.GetAttributeString(nameof(Name), string.Empty)!;
448  Description = element.GetAttributeString(nameof(Description), string.Empty)!;
449  MaxLevel = element.GetAttributeInt(nameof(MaxLevel), 1);
450  SuppressWarnings = element.GetAttributeBool(nameof(SuppressWarnings), false);
451  HideInMenus = element.GetAttributeBool(nameof(HideInMenus), false);
452  SourceElement = element;
453 
454  var targetProperties = new Dictionary<string, string[]>();
455  var maxLevels = new List<UpgradeMaxLevelMod>();
456  var resourceCosts = new HashSet<UpgradeResourceCost>();
457 
458  Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", "");
459  if (!nameIdentifier.IsEmpty)
460  {
461  Name = TextManager.Get($"UpgradeName.{nameIdentifier}");
462  }
463  else if (Name.IsNullOrWhiteSpace())
464  {
465  Name = TextManager.Get($"UpgradeName.{Identifier}");
466  }
467 
468  Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", "");
469  if (!descriptionIdentifier.IsEmpty)
470  {
471  Description = TextManager.Get($"UpgradeDescription.{descriptionIdentifier}");
472  }
473  else if (Description.IsNullOrWhiteSpace())
474  {
475  Description = TextManager.Get($"UpgradeDescription.{Identifier}");
476  }
477 
478  IncreaseOnTooltip = element.GetAttributeFloat("increaseontooltip", 0f);
479 
480  DebugConsole.Log(" " + Name);
481 
482 #if CLIENT
483  var decorativeSprites = new List<DecorativeSprite>();
484 #endif
485  foreach (var subElement in element.Elements())
486  {
487  switch (subElement.Name.ToString().ToLowerInvariant())
488  {
489  case "price":
490  {
491  Price = new UpgradePrice(this, subElement);
492  break;
493  }
494  case "maxlevel":
495  {
496  maxLevels.Add(new UpgradeMaxLevelMod(subElement));
497  break;
498  }
499  case "resourcecost":
500  {
501  resourceCosts.Add(new UpgradeResourceCost(subElement));
502  break;
503  }
504 #if CLIENT
505  case "decorativesprite":
506  {
507  decorativeSprites.Add(new DecorativeSprite(subElement));
508  break;
509  }
510  case "sprite":
511  {
512  Sprite = new Sprite(subElement);
513  break;
514  }
515 #else
516  case "decorativesprite":
517  case "sprite":
518  break;
519 #endif
520  default:
521  {
522  IEnumerable<string> properties = subElement.Attributes().Select(attribute => attribute.Name.ToString());
523  targetProperties.Add(subElement.Name.ToString(), properties.ToArray());
524  break;
525  }
526  }
527  }
528 
529 #if CLIENT
530  DecorativeSprites = decorativeSprites.ToImmutableArray();
531 #endif
532 
533  this.targetProperties = targetProperties;
534  MaxLevelsMods = maxLevels.ToImmutableArray();
535  ResourceCosts = resourceCosts.ToImmutableHashSet();
536 
537  upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty<Identifier>())?
538  .ToImmutableHashSet() ?? ImmutableHashSet<Identifier>.Empty;
539  }
540 
544  public int GetMaxLevelForCurrentSub()
545  {
546  Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub;
547  return sub is { Info: var info } ? GetMaxLevel(info) : MaxLevel;
548  }
549 
553  public int GetMaxLevel(SubmarineInfo info)
554  {
555  int level = MaxLevel;
556 
557  int tier = info.Tier;
558 
559  if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata)
560  {
561  int modifier = metadata.GetInt(new Identifier($"tiermodifiers.{Identifier}"), 0);
562  tier += modifier;
563  }
564 
565  tier = Math.Clamp(tier, 1, SubmarineInfo.HighestTier);
566 
567  foreach (UpgradeMaxLevelMod mod in MaxLevelsMods)
568  {
569  if (mod.AppliesTo(info.SubmarineClass, tier)) { level = mod.GetLevelAfter(level); }
570  }
571 
572  return level;
573  }
574 
575  public bool IsApplicable(SubmarineInfo? info)
576  {
577  if (info is null) { return false; }
578 
579  return GetMaxLevel(info) > 0;
580  }
581 
582  public bool HasResourcesToUpgrade(Character? character, int currentLevel)
583  {
584  if (character is null) { return false; }
585  if (!ResourceCosts.Any()) { return true; }
586 
587  var allItems = CargoManager.FindAllItemsOnPlayerAndSub(character);
588 
589  return ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)).All(cost => cost.Amount <= allItems.Count(cost.MatchesItem));
590  }
591 
592  // ReSharper disable PossibleMultipleEnumeration
593  public bool TryTakeResources(Character character, int currentLevel)
594  {
595  var costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel));
596 
597  if (!costs.Any()) { return true; }
598 
599  var inventoryItems = CargoManager.FindAllItemsOnPlayerAndSub(character);
600  HashSet<Item> itemsToRemove = new HashSet<Item>();
601 
602  foreach (UpgradeResourceCost cost in costs)
603  {
604  int amountNeeded = cost.Amount;
605  foreach (Item item in inventoryItems.Where(cost.MatchesItem))
606  {
607  itemsToRemove.Add(item);
608  amountNeeded--;
609  if (amountNeeded <= 0) { break; }
610  }
611 
612  if (amountNeeded > 0) { return false; }
613  }
614 
615  foreach (Item item in itemsToRemove)
616  {
617  Entity.Spawner.AddItemToRemoveQueue(item);
618  }
619 
620  if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); }
621 
622  return true;
623  }
624 
625  public ImmutableArray<ApplicableResourceCollection> GetApplicableResources(int level)
626  {
627  var applicableCosts = ResourceCosts.Where(cost => cost.AppliesForLevel(level)).ToImmutableHashSet();
628 
629  var costs = applicableCosts.Any()
630  ? applicableCosts.Select(ApplicableResourceCollection.CreateFor).ToImmutableArray()
631  : ImmutableArray<ApplicableResourceCollection>.Empty;
632 
633  return costs;
634  }
635 
636  public bool IsDisallowed(MapEntity item)
637  {
638  return item.DisallowedUpgradeSet.Contains(Identifier)
639  || UpgradeCategories.Any(c => item.DisallowedUpgradeSet.Contains(c.Identifier));
640  }
641 
642  public static UpgradePrefab? Find(Identifier identifier)
643  {
644  return identifier != Identifier.Empty ? Prefabs.Find(prefab => prefab.Identifier == identifier) : null;
645  }
646 
662  public static int ParsePercentage(string value, Identifier attribute = default, XElement? sourceElement = null, bool suppressWarnings = false)
663  {
664  string? line = sourceElement?.ToString().Split('\n')[0].Trim();
665  bool doWarnings = !suppressWarnings && !attribute.IsEmpty && sourceElement != null && line != null;
666 
667  if (string.IsNullOrWhiteSpace(value))
668  {
669  if (doWarnings)
670  {
671  DebugConsole.AddWarning($"Attribute \"{attribute}\" not found at {sourceElement!.Document?.ParseContentPathFromUri()} @ '{line}'.\n " +
672  "Value has been assumed to be '0'.");
673  }
674 
675  return 1;
676  }
677 
678  if (!int.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var price))
679  {
680  string str = value;
681 
682  if (str.Length > 1 && str[0] == '+') { str = str.Substring(1); }
683 
684  if (str.Length > 1 && str[^1] == '%') { str = str.Substring(0, str.Length - 1); }
685 
686  if (int.TryParse(str, out price))
687  {
688  return price;
689  }
690  }
691  else
692  {
693  return price;
694  }
695 
696  if (doWarnings)
697  {
698  DebugConsole.AddWarning($"Value in attribute \"{attribute}\" is not formatted correctly\n " +
699  $"at {sourceElement!.Document?.ParseContentPathFromUri()} @ '{line}'.\n " +
700  "It should be an integer with optionally a '+' or '-' at the front and/or '%' at the end.\n" +
701  "The value has been assumed to be '0'.");
702  }
703 
704  return 1;
705  }
706 
707  public override void Dispose()
708  {
709 #if CLIENT
710  Sprite?.Remove();
711  Sprite = null;
712  DecorativeSprites.ForEach(sprite => sprite.Remove());
713  targetProperties.Clear();
714 #endif
715  }
716  }
717 }
UpgradeContentPrefab(ContentXElement element, UpgradeModulesFile file)
static readonly PrefabCollection< UpgradeContentPrefab > PrefabsAndCategories
CharacterType
Definition: Enums.cs:711
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:195
FactionAffiliation
Definition: Factions.cs:9
@ Structure
Structures and hulls, but also items (for backwards support)!