Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Upgrades/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 UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file)
404  {
405  Name = element.GetAttributeString(nameof(Name), string.Empty)!;
406  Description = element.GetAttributeString(nameof(Description), string.Empty)!;
407  MaxLevel = element.GetAttributeInt(nameof(MaxLevel), 1);
408  SuppressWarnings = element.GetAttributeBool(nameof(SuppressWarnings), false);
409  HideInMenus = element.GetAttributeBool(nameof(HideInMenus), false);
410  SourceElement = element;
411 
412  var targetProperties = new Dictionary<string, string[]>();
413  var maxLevels = new List<UpgradeMaxLevelMod>();
414  var resourceCosts = new HashSet<UpgradeResourceCost>();
415 
416  Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", "");
417  if (!nameIdentifier.IsEmpty)
418  {
419  Name = TextManager.Get($"UpgradeName.{nameIdentifier}");
420  }
421  else if (Name.IsNullOrWhiteSpace())
422  {
423  Name = TextManager.Get($"UpgradeName.{Identifier}");
424  }
425 
426  Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", "");
427  if (!descriptionIdentifier.IsEmpty)
428  {
429  Description = TextManager.Get($"UpgradeDescription.{descriptionIdentifier}");
430  }
431  else if (Description.IsNullOrWhiteSpace())
432  {
433  Description = TextManager.Get($"UpgradeDescription.{Identifier}");
434  }
435 
436  IncreaseOnTooltip = element.GetAttributeFloat("increaseontooltip", 0f);
437 
438  DebugConsole.Log(" " + Name);
439 
440 #if CLIENT
441  var decorativeSprites = new List<DecorativeSprite>();
442 #endif
443  foreach (var subElement in element.Elements())
444  {
445  switch (subElement.Name.ToString().ToLowerInvariant())
446  {
447  case "price":
448  {
449  Price = new UpgradePrice(this, subElement);
450  break;
451  }
452  case "maxlevel":
453  {
454  maxLevels.Add(new UpgradeMaxLevelMod(subElement));
455  break;
456  }
457  case "resourcecost":
458  {
459  resourceCosts.Add(new UpgradeResourceCost(subElement));
460  break;
461  }
462 #if CLIENT
463  case "decorativesprite":
464  {
465  decorativeSprites.Add(new DecorativeSprite(subElement));
466  break;
467  }
468  case "sprite":
469  {
470  Sprite = new Sprite(subElement);
471  break;
472  }
473 #else
474  case "decorativesprite":
475  case "sprite":
476  break;
477 #endif
478  default:
479  {
480  IEnumerable<string> properties = subElement.Attributes().Select(attribute => attribute.Name.ToString());
481  targetProperties.Add(subElement.Name.ToString(), properties.ToArray());
482  break;
483  }
484  }
485  }
486 
487 #if CLIENT
488  DecorativeSprites = decorativeSprites.ToImmutableArray();
489 #endif
490 
491  this.targetProperties = targetProperties;
492  MaxLevelsMods = maxLevels.ToImmutableArray();
493  ResourceCosts = resourceCosts.ToImmutableHashSet();
494 
495  upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty<Identifier>())?
496  .ToImmutableHashSet() ?? ImmutableHashSet<Identifier>.Empty;
497  }
498 
502  public int GetMaxLevelForCurrentSub()
503  {
504  Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub;
505  return sub is { Info: var info } ? GetMaxLevel(info) : MaxLevel;
506  }
507 
511  public int GetMaxLevel(SubmarineInfo info)
512  {
513  int level = MaxLevel;
514 
515  int tier = info.Tier;
516 
517  if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata)
518  {
519  int modifier = metadata.GetInt(new Identifier($"tiermodifiers.{Identifier}"), 0);
520  tier += modifier;
521  }
522 
523  tier = Math.Clamp(tier, 1, SubmarineInfo.HighestTier);
524 
525  foreach (UpgradeMaxLevelMod mod in MaxLevelsMods)
526  {
527  if (mod.AppliesTo(info.SubmarineClass, tier)) { level = mod.GetLevelAfter(level); }
528  }
529 
530  return level;
531  }
532 
533  public bool IsApplicable(SubmarineInfo? info)
534  {
535  if (info is null) { return false; }
536 
537  return GetMaxLevel(info) > 0;
538  }
539 
540  public bool HasResourcesToUpgrade(Character? character, int currentLevel)
541  {
542  if (character is null) { return false; }
543  if (!ResourceCosts.Any()) { return true; }
544 
545  var allItems = CargoManager.FindAllItemsOnPlayerAndSub(character);
546 
547  return ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)).All(cost => cost.Amount <= allItems.Count(cost.MatchesItem));
548  }
549 
550  // ReSharper disable PossibleMultipleEnumeration
551  public bool TryTakeResources(Character character, int currentLevel)
552  {
553  var costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel));
554 
555  if (!costs.Any()) { return true; }
556 
557  var inventoryItems = CargoManager.FindAllItemsOnPlayerAndSub(character);
558  HashSet<Item> itemsToRemove = new HashSet<Item>();
559 
560  foreach (UpgradeResourceCost cost in costs)
561  {
562  int amountNeeded = cost.Amount;
563  foreach (Item item in inventoryItems.Where(cost.MatchesItem))
564  {
565  itemsToRemove.Add(item);
566  amountNeeded--;
567  if (amountNeeded <= 0) { break; }
568  }
569 
570  if (amountNeeded > 0) { return false; }
571  }
572 
573  foreach (Item item in itemsToRemove)
574  {
575  Entity.Spawner.AddItemToRemoveQueue(item);
576  }
577 
578  if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); }
579 
580  return true;
581  }
582 
583  public ImmutableArray<ApplicableResourceCollection> GetApplicableResources(int level)
584  {
585  var applicableCosts = ResourceCosts.Where(cost => cost.AppliesForLevel(level)).ToImmutableHashSet();
586 
587  var costs = applicableCosts.Any()
588  ? applicableCosts.Select(ApplicableResourceCollection.CreateFor).ToImmutableArray()
589  : ImmutableArray<ApplicableResourceCollection>.Empty;
590 
591  return costs;
592  }
593 
594  public bool IsDisallowed(MapEntity item)
595  {
596  return item.DisallowedUpgradeSet.Contains(Identifier)
597  || UpgradeCategories.Any(c => item.DisallowedUpgradeSet.Contains(c.Identifier));
598  }
599 
600  public static UpgradePrefab? Find(Identifier identifier)
601  {
602  return identifier != Identifier.Empty ? Prefabs.Find(prefab => prefab.Identifier == identifier) : null;
603  }
604 
620  public static int ParsePercentage(string value, Identifier attribute = default, XElement? sourceElement = null, bool suppressWarnings = false)
621  {
622  string? line = sourceElement?.ToString().Split('\n')[0].Trim();
623  bool doWarnings = !suppressWarnings && !attribute.IsEmpty && sourceElement != null && line != null;
624 
625  if (string.IsNullOrWhiteSpace(value))
626  {
627  if (doWarnings)
628  {
629  DebugConsole.AddWarning($"Attribute \"{attribute}\" not found at {sourceElement!.Document?.ParseContentPathFromUri()} @ '{line}'.\n " +
630  "Value has been assumed to be '0'.");
631  }
632 
633  return 1;
634  }
635 
636  if (!int.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var price))
637  {
638  string str = value;
639 
640  if (str.Length > 1 && str[0] == '+') { str = str.Substring(1); }
641 
642  if (str.Length > 1 && str[^1] == '%') { str = str.Substring(0, str.Length - 1); }
643 
644  if (int.TryParse(str, out price))
645  {
646  return price;
647  }
648  }
649  else
650  {
651  return price;
652  }
653 
654  if (doWarnings)
655  {
656  DebugConsole.AddWarning($"Value in attribute \"{attribute}\" is not formatted correctly\n " +
657  $"at {sourceElement!.Document?.ParseContentPathFromUri()} @ '{line}'.\n " +
658  "It should be an integer with optionally a '+' or '-' at the front and/or '%' at the end.\n" +
659  "The value has been assumed to be '0'.");
660  }
661 
662  return 1;
663  }
664 
665  public override void Dispose()
666  {
667 #if CLIENT
668  Sprite?.Remove();
669  Sprite = null;
670  DecorativeSprites.ForEach(sprite => sprite.Remove());
671  targetProperties.Clear();
672 #endif
673  }
674  }
675 }
UpgradeContentPrefab(ContentXElement element, UpgradeModulesFile file)
static readonly PrefabCollection< UpgradeContentPrefab > PrefabsAndCategories
CharacterType
Definition: Enums.cs:685
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180
FactionAffiliation
Definition: Factions.cs:9