Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/ItemPrefab.cs
2 using Barotrauma.IO;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Collections.Immutable;
8 using System.Linq;
9 using System.Security.Cryptography;
10 using System.Xml.Linq;
11 
12 namespace Barotrauma
13 {
14  readonly struct SkillRequirementHint
15  {
16  public readonly Identifier Skill;
17  public readonly float Level;
18  public readonly LocalizedString SkillName;
19 
20  public LocalizedString GetFormattedText(int skillLevel, string levelColorTag) =>
21  $"{SkillName} {Level} (‖color:{levelColorTag}‖{skillLevel}‖color:end‖)";
22 
24  {
25  Skill = element.GetAttributeIdentifier("identifier", Identifier.Empty);
26  Level = element.GetAttributeFloat("level", 0);
27  SkillName = TextManager.Get("skillname." + Skill);
28  }
29  }
30 
31  readonly struct DeconstructItem
32  {
33  public readonly Identifier ItemIdentifier;
34  //number of items to output
35  public readonly int Amount;
36  //minCondition does <= check, meaning that below or equal to min condition will be skipped.
37  public readonly float MinCondition;
38  //maxCondition does > check, meaning that above this max the deconstruct item will be skipped.
39  public readonly float MaxCondition;
40  //Condition of item on creation
41  public readonly float OutConditionMin, OutConditionMax;
42  //should the condition of the deconstructed item be copied to the output items
43  public readonly bool CopyCondition;
44 
45  //tag/identifier of the deconstructor(s) that can be used to deconstruct the item into this
46  public readonly Identifier[] RequiredDeconstructor;
47  //tag/identifier of other item(s) that that need to be present in the deconstructor to deconstruct the item into this
48  public readonly Identifier[] RequiredOtherItem;
49  //text to display on the deconstructor's activate button when this output is available
50  public readonly string ActivateButtonText;
51  public readonly string InfoText;
52  public readonly string InfoTextOnOtherItemMissing;
53 
54  public readonly float Commonness;
55 
56  public DeconstructItem(XElement element, Identifier parentDebugName)
57  {
58  ItemIdentifier = element.GetAttributeIdentifier("identifier", "");
59  Amount = element.GetAttributeInt("amount", 1);
60  MinCondition = element.GetAttributeFloat("mincondition", -0.1f);
61  MaxCondition = element.GetAttributeFloat("maxcondition", 1.0f);
62  OutConditionMin = element.GetAttributeFloat("outconditionmin", element.GetAttributeFloat("outcondition", 1.0f));
63  OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f));
64  CopyCondition = element.GetAttributeBool("copycondition", false);
65  Commonness = element.GetAttributeFloat("commonness", 1.0f);
66  RequiredDeconstructor = element.GetAttributeIdentifierArray("requireddeconstructor",
67  element.Parent?.GetAttributeIdentifierArray("requireddeconstructor", Array.Empty<Identifier>()) ?? Array.Empty<Identifier>());
68  RequiredOtherItem = element.GetAttributeIdentifierArray("requiredotheritem", Array.Empty<Identifier>());
69  ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty);
70  InfoText = element.GetAttributeString("infotext", string.Empty);
71  InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty);
72  }
73 
74  public bool IsValidDeconstructor(Item deconstructor)
75  {
76  return RequiredDeconstructor.Length == 0 || RequiredDeconstructor.Any(r => deconstructor.HasTag(r) || deconstructor.Prefab.Identifier == r);
77  }
78  }
79 
81  {
82  public abstract class RequiredItem
83  {
84  public abstract IEnumerable<ItemPrefab> ItemPrefabs { get; }
85  public abstract UInt32 UintIdentifier { get; }
86 
87  public abstract bool MatchesItem(Item item);
88 
89  public abstract ItemPrefab FirstMatchingPrefab { get; }
90 
93 
94  public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem)
95  {
96  Amount = amount;
97  MinCondition = minCondition;
98  MaxCondition = maxCondition;
99  UseCondition = useCondition;
100  OverrideHeader = overrideHeader;
101  OverrideDescription = overrideDescription;
102  DefaultItem = defaultItem;
103  }
104  public readonly int Amount;
105  public readonly float MinCondition;
106  public readonly float MaxCondition;
107  public readonly bool UseCondition;
111  public readonly Identifier DefaultItem;
112 
113  public bool IsConditionSuitable(float conditionPercentage)
114  {
115  float normalizedCondition = conditionPercentage / 100.0f;
116  if (MathUtils.NearlyEqual(normalizedCondition, MinCondition) || MathUtils.NearlyEqual(normalizedCondition, MaxCondition))
117  {
118  return true;
119  }
120  else if (normalizedCondition >= MinCondition && normalizedCondition <= MaxCondition)
121  {
122  return true;
123  }
124  return false;
125  }
126  }
127 
129  {
130  public readonly Identifier ItemPrefabIdentifier;
131 
133  ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab
135 
136  public override UInt32 UintIdentifier { get; }
137 
138  public override IEnumerable<ItemPrefab> ItemPrefabs => ItemPrefab == null ? Enumerable.Empty<ItemPrefab>() : ItemPrefab.ToEnumerable();
139 
141 
142 
143  public override bool MatchesItem(Item item)
144  {
145  return item?.Prefab.Identifier == ItemPrefabIdentifier;
146  }
147 
148  public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader) :
149  base(amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem: Identifier.Empty)
150  {
151  ItemPrefabIdentifier = itemPrefab;
152  using MD5 md5 = MD5.Create();
153  UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(itemPrefab, md5);
154  }
155 
156  public override string ToString()
157  {
158  return $"{base.ToString()} ({ItemPrefabIdentifier})";
159  }
160  }
161 
163  {
164  public readonly Identifier Tag;
165 
166  public override UInt32 UintIdentifier { get; }
167 
168  private readonly List<ItemPrefab> cachedPrefabs = new List<ItemPrefab>();
169 
170  private Md5Hash prevContentPackagesHash;
171 
172  public override IEnumerable<ItemPrefab> ItemPrefabs
173  {
174  get
175  {
176  if (prevContentPackagesHash == null ||
177  !prevContentPackagesHash.Equals(ContentPackageManager.EnabledPackages.MergedHash))
178  {
179  cachedPrefabs.Clear();
180  cachedPrefabs.AddRange(ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag)));
181  prevContentPackagesHash = ContentPackageManager.EnabledPackages.MergedHash;
182  }
183  return cachedPrefabs;
184  }
185  }
186 
187  public override ItemPrefab FirstMatchingPrefab => ItemPrefabs.FirstOrDefault();
188 
189  public override bool MatchesItem(Item item)
190  {
191  if (item == null) { return false; }
192  return item.HasTag(Tag);
193  }
194 
195  public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem)
196  : base(amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem)
197  {
198  Tag = tag;
199  using MD5 md5 = MD5.Create();
200  UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(tag, md5);
201  }
202 
203  public override string ToString()
204  {
205  return $"{base.ToString()} ({Tag})";
206  }
207  }
208 
209  public readonly Identifier TargetItemPrefabIdentifier;
211 
212  private readonly Lazy<LocalizedString> displayName;
214  => ItemPrefab.Prefabs.ContainsKey(TargetItemPrefabIdentifier) ? displayName.Value : "";
215  public readonly ImmutableArray<RequiredItem> RequiredItems;
216  public readonly ImmutableArray<Identifier> SuitableFabricatorIdentifiers;
217  public readonly float RequiredTime;
218  public readonly int RequiredMoney;
219  public readonly bool RequiresRecipe;
220  public readonly float OutCondition; //Percentage-based from 0 to 1
221  public readonly ImmutableArray<Skill> RequiredSkills;
222  public readonly uint RecipeHash;
223  public readonly int Amount;
224  public readonly int? Quality;
225  public readonly bool HideForNonTraitors;
226 
230  public readonly int FabricationLimitMin, FabricationLimitMax;
231 
232  public FabricationRecipe(ContentXElement element, Identifier itemPrefab)
233  {
234  TargetItemPrefabIdentifier = itemPrefab;
235  var displayNameIdentifier = element.GetAttributeIdentifier("displayname", "");
236  displayName = new Lazy<LocalizedString>(() => displayNameIdentifier.IsEmpty
237  ? TargetItem.Name
238  : TextManager.GetWithVariable($"DisplayName.{displayNameIdentifier}", "[itemname]", TargetItem.Name));
239 
240  SuitableFabricatorIdentifiers = element.GetAttributeIdentifierArray("suitablefabricators", Array.Empty<Identifier>()).ToImmutableArray();
241 
242  var requiredSkills = new List<Skill>();
243  RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f);
244  RequiredMoney = element.GetAttributeInt("requiredmoney", 0);
245  OutCondition = element.GetAttributeFloat("outcondition", 1.0f);
246  if (OutCondition > 1.0f)
247  {
248  DebugConsole.AddWarning($"Error in \"{itemPrefab}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100}).",
249  element.ContentPackage);
250  }
251  var requiredItems = new List<RequiredItem>();
252  RequiresRecipe = element.GetAttributeBool("requiresrecipe", false);
253  Amount = element.GetAttributeInt("amount", 1);
254 
255  int limitDefault = element.GetAttributeInt("fabricationlimit", -1);
256  FabricationLimitMin = element.GetAttributeInt(nameof(FabricationLimitMin), limitDefault);
257  FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault);
258 
259  HideForNonTraitors = element.GetAttributeBool(nameof(HideForNonTraitors), false);
260 
261  if (element.GetAttribute(nameof(Quality)) != null)
262  {
263  Quality = element.GetAttributeInt(nameof(Quality), 0);
264  }
265 
266  foreach (var subElement in element.Elements())
267  {
268  switch (subElement.Name.ToString().ToLowerInvariant())
269  {
270  case "requiredskill":
271  if (subElement.GetAttribute("name") != null)
272  {
273  DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! Use skill identifiers instead of names.",
274  contentPackage: element.ContentPackage);
275  continue;
276  }
277 
278  requiredSkills.Add(new Skill(
279  subElement.GetAttributeIdentifier("identifier", ""),
280  subElement.GetAttributeInt("level", 0)));
281  break;
282  case "item":
283  case "requireditem":
284  Identifier requiredItemIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
285  Identifier requiredItemTag = subElement.GetAttributeIdentifier("tag", Identifier.Empty);
286  if (requiredItemIdentifier == Identifier.Empty && requiredItemTag == Identifier.Empty)
287  {
288  DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! One of the required items has no identifier or tag.",
289  contentPackage: element.ContentPackage);
290  continue;
291  }
292 
293  float minCondition = subElement.GetAttributeFloat("mincondition", 1.0f);
294  float maxCondition = subElement.GetAttributeFloat("maxcondition", 1.0f);
295  //Substract mincondition from required item's condition or delete it regardless?
296  bool useCondition = subElement.GetAttributeBool("usecondition", true);
297  int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1));
298 
299  LocalizedString overrideDescription = string.Empty;
300  if (subElement.GetAttributeString("description", string.Empty) is string descriptionTag && !descriptionTag.IsNullOrEmpty())
301  {
302  overrideDescription = TextManager.Get(descriptionTag);
303  }
304  LocalizedString overrideHeader = string.Empty;
305  if (subElement.GetAttributeString("header", string.Empty) is string headerTag && !headerTag.IsNullOrEmpty())
306  {
307  overrideHeader = TextManager.Get(headerTag);
308  }
309 
310  if (requiredItemIdentifier != Identifier.Empty)
311  {
312  var existing = requiredItems.FindIndex(r =>
313  r is RequiredItemByIdentifier ri &&
314  ri.ItemPrefabIdentifier == requiredItemIdentifier &&
315  MathUtils.NearlyEqual(r.MinCondition, minCondition) &&
316  MathUtils.NearlyEqual(r.MaxCondition, maxCondition));
317  if (existing >= 0)
318  {
319  amount += requiredItems[existing].Amount;
320  requiredItems.RemoveAt(existing);
321  }
322  requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader));
323  }
324  else
325  {
326  var existing = requiredItems.FindIndex(r =>
327  r is RequiredItemByTag rt &&
328  rt.Tag == requiredItemTag &&
329  MathUtils.NearlyEqual(r.MinCondition, minCondition) &&
330  MathUtils.NearlyEqual(r.MaxCondition, maxCondition));
331  if (existing >= 0)
332  {
333  amount += requiredItems[existing].Amount;
334  requiredItems.RemoveAt(existing);
335  }
336  Identifier defaultItem = subElement.GetAttributeIdentifier("defaultitem", Identifier.Empty);
337  requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem));
338  }
339  break;
340  }
341  }
342 
343  RequiredSkills = requiredSkills.ToImmutableArray();
344  RequiredItems = requiredItems
345  /*Put the items required by identifier first - since we must use specific items for those, we should check them before the ones that accept multiple items.
346  Otherwise we might end up choosing the "specific item" as the multi-option ingredient, and not have enough left for the "specific item" requirement */
347  .OrderBy(requiredItem => requiredItem is RequiredItemByIdentifier ? 0 : 1)
348  .ToImmutableArray();
349 
350  RecipeHash = GenerateHash();
351  }
352 
353  private uint GenerateHash()
354  {
355  using var md5 = MD5.Create();
356  uint outputId = ToolBoxCore.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5);
357 
358  var requiredItems = string.Join(':', RequiredItems
359  .Select(static i => $"{i.UintIdentifier}:{i.Amount}")
360  .Select(static i => string.Join(',', i)));
361  // above but include the item amount
362 
363  var requiredSkills = string.Join(':', RequiredSkills.Select(s => $"{s.Identifier}:{s.Level}"));
364 
365  uint retVal = ToolBoxCore.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{RequiresRecipe}|{requiredItems}|{requiredSkills}", md5);
366  if (retVal == 0) { retVal = 1; }
367  return retVal;
368  }
369  }
370 
372  {
373  public readonly ImmutableHashSet<Identifier> Primary;
374  public readonly ImmutableHashSet<Identifier> Secondary;
375 
376  public readonly float SpawnProbability;
377  public readonly float MaxCondition;
378  public readonly float MinCondition;
379  public readonly int MinAmount;
380  public readonly int MaxAmount;
381  // Overrides min and max, if defined.
382  public readonly int Amount;
383  public readonly bool CampaignOnly;
384  public readonly bool NotCampaign;
385  public readonly bool TransferOnlyOnePerContainer;
386  public readonly bool AllowTransfersHere = true;
387 
388  public readonly float MinLevelDifficulty, MaxLevelDifficulty;
389 
390  public PreferredContainer(XElement element)
391  {
392  Primary = XMLExtensions.GetAttributeIdentifierArray(element, "primary", Array.Empty<Identifier>()).ToImmutableHashSet();
393  Secondary = XMLExtensions.GetAttributeIdentifierArray(element, "secondary", Array.Empty<Identifier>()).ToImmutableHashSet();
394  SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f);
395  MinAmount = element.GetAttributeInt("minamount", 0);
396  MaxAmount = Math.Max(MinAmount, element.GetAttributeInt("maxamount", 0));
397  Amount = element.GetAttributeInt("amount", 0);
398  MaxCondition = element.GetAttributeFloat("maxcondition", 100f);
399  MinCondition = element.GetAttributeFloat("mincondition", 0f);
400  CampaignOnly = element.GetAttributeBool("campaignonly", CampaignOnly);
401  NotCampaign = element.GetAttributeBool("notcampaign", NotCampaign);
402  TransferOnlyOnePerContainer = element.GetAttributeBool("TransferOnlyOnePerContainer", TransferOnlyOnePerContainer);
403  AllowTransfersHere = element.GetAttributeBool("AllowTransfersHere", AllowTransfersHere);
404 
405  MinLevelDifficulty = element.GetAttributeFloat(nameof(MinLevelDifficulty), float.MinValue);
406  MaxLevelDifficulty = element.GetAttributeFloat(nameof(MaxLevelDifficulty), float.MaxValue);
407 
408  if (element.GetAttribute("spawnprobability") == null)
409  {
410  //if spawn probability is not defined but amount is, assume the probability is 1
411  if (MaxAmount > 0 || Amount > 0)
412  {
413  SpawnProbability = 1.0f;
414  }
415  }
416  else if (element.GetAttribute("minamount") == null && element.GetAttribute("maxamount") == null && element.GetAttribute("amount") == null)
417  {
418  //spawn probability defined but amount isn't, assume amount is 1
419  MinAmount = MaxAmount = Amount = 1;
420  SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f);
421  }
422  }
423  }
424 
426  {
427  public int BasePrice { get; }
428 
429  public readonly bool CanBeBought;
430 
431  public readonly Identifier ReplacementOnUninstall;
432 
433  public string SpawnWithId;
434 
435  public string SwapIdentifier;
436 
437  public readonly Vector2 SwapOrigin;
438 
439  public List<(Identifier requiredTag, Identifier swapTo)> ConnectedItemsToSwap = new List<(Identifier requiredTag, Identifier swapTo)>();
440 
441  public readonly Sprite SchematicSprite;
442 
443  public int GetPrice(Location location = null)
444  {
445  int price = location?.GetAdjustedMechanicalCost(BasePrice) ?? BasePrice;
446  if (GameMain.GameSession?.Campaign is CampaignMode campaign)
447  {
448  price = (int)(price * campaign.Settings.ShipyardPriceMultiplier);
449  }
450  return price;
451  }
452 
454  {
455  BasePrice = Math.Max(element.GetAttributeInt("price", 0), 0);
456  SwapIdentifier = element.GetAttributeString("swapidentifier", string.Empty);
457  CanBeBought = element.GetAttributeBool("canbebought", BasePrice != 0);
458  ReplacementOnUninstall = element.GetAttributeIdentifier("replacementonuninstall", "");
459  SwapOrigin = element.GetAttributeVector2("origin", Vector2.One);
460  SpawnWithId = element.GetAttributeString("spawnwithid", string.Empty);
461 
462  foreach (var subElement in element.Elements())
463  {
464  switch (subElement.Name.ToString().ToLowerInvariant())
465  {
466  case "schematicsprite":
467  SchematicSprite = new Sprite(subElement);
468  break;
469  case "swapconnecteditem":
471  (subElement.GetAttributeIdentifier("tag", ""),
472  subElement.GetAttributeIdentifier("swapto", "")));
473  break;
474  }
475  }
476  }
477  }
478 
479  partial class ItemPrefab : MapEntityPrefab, IImplementsVariants<ItemPrefab>
480  {
482 
483  public const float DefaultInteractDistance = 120.0f;
484 
485  //default size
486  public Vector2 Size { get; private set; }
487 
488  private PriceInfo defaultPrice;
489  public PriceInfo DefaultPrice => defaultPrice;
490  private ImmutableDictionary<Identifier, PriceInfo> StorePrices { get; set; }
491  public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) ||
492  (StorePrices != null && StorePrices.Any(p => p.Value.CanBeBought));
496  public bool CanBeSold => DefaultPrice != null;
497 
502  public ImmutableArray<Rectangle> Triggers { get; private set; }
503 
504  private ImmutableDictionary<Identifier, float> treatmentSuitability;
505 
509  public bool IsOverride => Prefabs.IsOverride(this);
510 
511  private readonly XElement originalElement;
512  public ContentXElement ConfigElement { get; private set; }
513 
514  public ImmutableArray<DeconstructItem> DeconstructItems { get; private set; }
515 
516  public ImmutableDictionary<uint, FabricationRecipe> FabricationRecipes { get; private set; }
517 
518  public float DeconstructTime { get; private set; }
519 
520  public bool AllowDeconstruct { get; private set; }
521 
522  //Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced.
523  public ImmutableArray<PreferredContainer> PreferredContainers { get; private set; }
524 
525  public ImmutableArray<SkillRequirementHint> SkillRequirementHints { get; private set; }
526 
528  {
529  get;
530  private set;
531  }
532 
533  public readonly struct CommonnessInfo
534  {
535  public float Commonness
536  {
537  get
538  {
539  return commonness;
540  }
541  }
542  public float AbyssCommonness
543  {
544  get
545  {
546  return abyssCommonness ?? 0.0f;
547  }
548  }
549  public float CaveCommonness
550  {
551  get
552  {
553  return caveCommonness ?? Commonness;
554  }
555  }
556  public bool CanAppear
557  {
558  get
559  {
560  if (Commonness > 0.0f) { return true; }
561  if (AbyssCommonness > 0.0f) { return true; }
562  if (CaveCommonness > 0.0f) { return true; }
563  return false;
564  }
565  }
566 
567  public readonly float commonness;
568  public readonly float? abyssCommonness;
569  public readonly float? caveCommonness;
570 
571  public CommonnessInfo(XElement element)
572  {
573  this.commonness = Math.Max(element?.GetAttributeFloat("commonness", 0.0f) ?? 0.0f, 0.0f);
574 
575  float? abyssCommonness = null;
576  XAttribute abyssCommonnessAttribute = element?.GetAttribute("abysscommonness") ?? element?.GetAttribute("abyss");
577  if (abyssCommonnessAttribute != null)
578  {
579  abyssCommonness = Math.Max(abyssCommonnessAttribute.GetAttributeFloat(0.0f), 0.0f);
580  }
581  this.abyssCommonness = abyssCommonness;
582 
583  float? caveCommonness = null;
584  XAttribute caveCommonnessAttribute = element?.GetAttribute("cavecommonness") ?? element?.GetAttribute("cave");
585  if (caveCommonnessAttribute != null)
586  {
587  caveCommonness = Math.Max(caveCommonnessAttribute.GetAttributeFloat(0.0f), 0.0f);
588  }
589  this.caveCommonness = caveCommonness;
590  }
591 
593  {
594  this.commonness = commonness;
595  this.abyssCommonness = abyssCommonness != null ? (float?)Math.Max(abyssCommonness.Value, 0.0f) : null;
596  this.caveCommonness = caveCommonness != null ? (float?)Math.Max(caveCommonness.Value, 0.0f) : null;
597  }
598 
600  {
601  return new CommonnessInfo(commonness,
602  abyssCommonness ?? parentInfo?.abyssCommonness,
603  caveCommonness ?? parentInfo?.caveCommonness);
604  }
605 
607  {
608  CommonnessInfo info = this;
609  foreach (var parentInfo in parentInfos)
610  {
611  info = info.WithInheritedCommonness(parentInfo);
612  }
613  return info;
614  }
615 
616  public float GetCommonness(Level.TunnelType tunnelType)
617  {
618  if (tunnelType == Level.TunnelType.Cave)
619  {
620  return CaveCommonness;
621  }
622  else
623  {
624  return Commonness;
625  }
626  }
627  }
628 
632  private ImmutableDictionary<Identifier, CommonnessInfo> LevelCommonness { get; set; }
633 
634  public readonly struct FixedQuantityResourceInfo
635  {
636  public readonly int ClusterQuantity;
637  public readonly int ClusterSize;
638  public readonly bool IsIslandSpecific;
639  public readonly bool AllowAtStart;
640 
641  public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific, bool allowAtStart)
642  {
643  ClusterQuantity = clusterQuantity;
644  ClusterSize = clusterSize;
645  IsIslandSpecific = isIslandSpecific;
646  AllowAtStart = allowAtStart;
647  }
648  }
649 
650  public ImmutableDictionary<Identifier, FixedQuantityResourceInfo> LevelQuantity { get; private set; }
651 
652  private bool canSpriteFlipX;
653  public override bool CanSpriteFlipX => canSpriteFlipX;
654 
655  private bool canSpriteFlipY;
656  public override bool CanSpriteFlipY => canSpriteFlipY;
657 
661  public bool? AllowAsExtraCargo { get; private set; }
662 
663  public bool RandomDeconstructionOutput { get; private set; }
664 
665  public int RandomDeconstructionOutputAmount { get; private set; }
666 
667  private Sprite sprite;
668  public override Sprite Sprite => sprite;
669 
670  public override string OriginalName { get; }
671 
672  private LocalizedString name;
673  public override LocalizedString Name => name;
674 
675  private ImmutableHashSet<Identifier> tags;
676  public override ImmutableHashSet<Identifier> Tags => tags;
677 
678  private ImmutableHashSet<Identifier> allowedLinks;
679  public override ImmutableHashSet<Identifier> AllowedLinks => allowedLinks;
680 
681  private MapEntityCategory category;
682  public override MapEntityCategory Category => category;
683 
684  private ImmutableHashSet<string> aliases;
685  public override ImmutableHashSet<string> Aliases => aliases;
686 
687  //how close the Character has to be to the item to pick it up
689  public float InteractDistance { get; private set; }
690 
691  // this can be used to allow items which are behind other items tp
692  [Serialize(0.0f, IsPropertySaveable.No)]
693  public float InteractPriority { get; private set; }
694 
695  [Serialize(false, IsPropertySaveable.No)]
696  public bool InteractThroughWalls { get; private set; }
697 
698  [Serialize(false, IsPropertySaveable.No, description: "Hides the condition bar displayed at the bottom of the inventory slot the item is in.")]
699  public bool HideConditionBar { get; set; }
700 
701  [Serialize(false, IsPropertySaveable.No, description: "Hides the condition displayed in the item's tooltip.")]
702  public bool HideConditionInTooltip { get; set; }
703 
704  //if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item
705  //if false, trigger areas define areas that can be used to highlight the item
706  [Serialize(true, IsPropertySaveable.No)]
707  public bool RequireBodyInsideTrigger { get; private set; }
708 
709  //if true and the item has trigger areas defined, players can only highlight the item when the cursor is on the trigger
710  [Serialize(false, IsPropertySaveable.No)]
711  public bool RequireCursorInsideTrigger { get; private set; }
712 
713  //if true then players can only highlight the item if its targeted for interaction by a campaign event
714  [Serialize(false, IsPropertySaveable.No)]
716  {
717  get;
718  private set;
719  }
720 
721  //should the camera focus on the item when selected
722  [Serialize(false, IsPropertySaveable.No)]
723  public bool FocusOnSelected { get; private set; }
724 
725  //the amount of "camera offset" when selecting the construction
726  [Serialize(0.0f, IsPropertySaveable.No)]
727  public float OffsetOnSelected { get; private set; }
728 
729  [Serialize(false, IsPropertySaveable.No, description: "Should the character who's selected the item grab it (hold their hand on it, the same way as they do when repairing)? Defaults to true on items that have an ItemContainer component.")]
730  public bool GrabWhenSelected { get; set; }
731 
732  private float health;
733 
734  [Serialize(100.0f, IsPropertySaveable.No)]
735  public float Health
736  {
737  get { return health; }
738  private set
739  {
740  //don't allow health values higher than this, because they lead to various issues:
741  //e.g. integer overflows when we're casting to int to display a health value, value being set to float.Infinity if it's high enough
742  health = Math.Min(value, 1000000.0f);
743  }
744  }
745 
746  [Serialize(false, IsPropertySaveable.No)]
747  public bool AllowSellingWhenBroken { get; private set; }
748 
749  [Serialize(false, IsPropertySaveable.No)]
750  public bool AllowStealingAlways { get; private set; }
751 
752  [Serialize(false, IsPropertySaveable.No)]
753  public bool Indestructible { get; private set; }
754 
755  [Serialize(false, IsPropertySaveable.No)]
756  public bool DamagedByExplosions { get; private set; }
757 
758  [Serialize(1f, IsPropertySaveable.No)]
759  public float ExplosionDamageMultiplier { get; private set; }
760 
761  [Serialize(1f, IsPropertySaveable.No)]
762  public float ItemDamageMultiplier { get; private set; }
763 
764  [Serialize(false, IsPropertySaveable.No)]
765  public bool DamagedByProjectiles { get; private set; }
766 
767  [Serialize(false, IsPropertySaveable.No)]
768  public bool DamagedByMeleeWeapons { get; private set; }
769 
770  [Serialize(false, IsPropertySaveable.No)]
771  public bool DamagedByRepairTools { get; private set; }
772 
773  [Serialize(false, IsPropertySaveable.No)]
774  public bool DamagedByMonsters { get; private set; }
775 
776  private float impactTolerance;
777  [Serialize(0.0f, IsPropertySaveable.No)]
778  public float ImpactTolerance
779  {
780  get { return impactTolerance; }
781  set { impactTolerance = Math.Max(value, 0.0f); }
782  }
783 
784  [Serialize(0.0f, IsPropertySaveable.No)]
785  public float OnDamagedThreshold { get; set; }
786 
787  [Serialize(0.0f, IsPropertySaveable.No)]
788  public float SonarSize
789  {
790  get;
791  private set;
792  }
793 
794  [Serialize(false, IsPropertySaveable.No)]
795  public bool UseInHealthInterface { get; private set; }
796 
797  [Serialize(false, IsPropertySaveable.No)]
798  public bool DisableItemUsageWhenSelected { get; private set; }
799 
800  [Serialize("metalcrate", IsPropertySaveable.No)]
801  public string CargoContainerIdentifier { get; private set; }
802 
803  [Serialize(false, IsPropertySaveable.No)]
804  public bool UseContainedSpriteColor { get; private set; }
805 
806  [Serialize(false, IsPropertySaveable.No)]
807  public bool UseContainedInventoryIconColor { get; private set; }
808 
809  [Serialize(0.0f, IsPropertySaveable.No)]
811  {
812  get;
813  private set;
814  }
815 
816  [Serialize(0.0f, IsPropertySaveable.No)]
818  {
819  get;
820  private set;
821  }
822 
823  [Serialize(false, IsPropertySaveable.No)]
824  public bool CannotRepairFail
825  {
826  get;
827  private set;
828  }
829 
830  [Serialize(null, IsPropertySaveable.No)]
831  public string EquipConfirmationText { get; set; }
832 
833  [Serialize(true, IsPropertySaveable.No, description: "Can the item be rotated in the submarine editor?")]
834  public bool AllowRotatingInEditor { get; set; }
835 
836  [Serialize(false, IsPropertySaveable.No)]
837  public bool ShowContentsInTooltip { get; private set; }
838 
839  [Serialize(true, IsPropertySaveable.No)]
840  public bool CanFlipX { get; private set; }
841 
842  [Serialize(true, IsPropertySaveable.No)]
843  public bool CanFlipY { get; private set; }
844 
845  [Serialize(0.01f, IsPropertySaveable.No)]
846  public float MinScale { get; private set; }
847 
848  [Serialize(10.0f, IsPropertySaveable.No)]
849  public float MaxScale { get; private set; }
850 
851  [Serialize(false, IsPropertySaveable.No)]
852  public bool IsDangerous { get; private set; }
853 
854  private int maxStackSize;
856  public int MaxStackSize
857  {
858  get { return maxStackSize; }
859  private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxPossibleStackSize); }
860  }
861 
862  private int maxStackSizeCharacterInventory;
863  [Serialize(-1, IsPropertySaveable.No, description: "Maximum stack size when the item is in a character inventory.")]
865  {
866  get { return maxStackSizeCharacterInventory; }
867  private set { maxStackSizeCharacterInventory = Math.Min(value, Inventory.MaxPossibleStackSize); }
868  }
869 
870  private int maxStackSizeHoldableOrWearableInventory;
871  [Serialize(-1, IsPropertySaveable.No, description:
872  "Maximum stack size when the item is inside a holdable or wearable item. "+
873  "If not set, defaults to MaxStackSizeCharacterInventory.")]
875  {
876  get { return maxStackSizeHoldableOrWearableInventory; }
877  private set { maxStackSizeHoldableOrWearableInventory = Math.Min(value, Inventory.MaxPossibleStackSize); }
878  }
879 
880  public int GetMaxStackSize(Inventory inventory)
881  {
882  int extraStackSize = inventory switch
883  {
884  ItemInventory { Owner: Item it } i => (int)it.StatManager.GetAdjustedValueAdditive(ItemTalentStats.ExtraStackSize, i.ExtraStackSize),
885  CharacterInventory { Owner: Character { Info: { } info } } i => i.ExtraStackSize + (int)info.GetSavedStatValueWithAll(StatTypes.InventoryExtraStackSize, Category.ToIdentifier()),
886  not null => inventory.ExtraStackSize,
887  null => 0
888  };
889 
890  if (inventory is CharacterInventory && maxStackSizeCharacterInventory > 0)
891  {
892  return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize);
893  }
894  else if (inventory?.Owner is Item item &&
895  (item.GetComponent<Holdable>() is { Attachable: false } || item.GetComponent<Wearable>() != null))
896  {
897  if (maxStackSizeHoldableOrWearableInventory > 0)
898  {
899  return MaxStackWithExtra(maxStackSizeHoldableOrWearableInventory, extraStackSize);
900  }
901  else if (maxStackSizeCharacterInventory > 0)
902  {
903  //if maxStackSizeHoldableOrWearableInventory is not set, it defaults to maxStackSizeCharacterInventory
904  return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize);
905  }
906  }
907 
908  return MaxStackWithExtra(maxStackSize, extraStackSize);
909 
910  static int MaxStackWithExtra(int maxStackSize, int extraStackSize)
911  {
912  extraStackSize = Math.Max(extraStackSize, 0);
913  if (maxStackSize == 1)
914  {
915  return Math.Min(maxStackSize, Inventory.MaxPossibleStackSize);
916  }
917  return Math.Min(maxStackSize + extraStackSize, Inventory.MaxPossibleStackSize);
918  }
919  }
920 
921  [Serialize(false, IsPropertySaveable.No)]
922  public bool AllowDroppingOnSwap { get; private set; }
923 
924  public ImmutableHashSet<Identifier> AllowDroppingOnSwapWith { get; private set; }
925 
926  [Serialize(false, IsPropertySaveable.No, "If enabled, the item is not transferred when the player transfers items between subs.")]
927  public bool DontTransferBetweenSubs { get; private set; }
928 
929  [Serialize(true, IsPropertySaveable.No)]
930  public bool ShowHealthBar { get; private set; }
931 
932  [Serialize(1f, IsPropertySaveable.No, description: "How much the bots prioritize this item when they seek for items. For example, bots prioritize less exosuit than the other diving suits. Defaults to 1. Note that there's also a specific CombatPriority for items that can be used as weapons.")]
933  public float BotPriority { get; private set; }
934 
935  [Serialize(true, IsPropertySaveable.No)]
936  public bool ShowNameInHealthBar { get; private set; }
937 
938  [Serialize(false, IsPropertySaveable.No, description:"Should the bots shoot at this item with turret or not? Disabled by default.")]
939  public bool IsAITurretTarget { get; private set; }
940 
941  [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with turrets? Defaults to 1. Distance to the target affects the decision making.")]
942  public float AITurretPriority { get; private set; }
943 
944  [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")]
945  public float AISlowTurretPriority { get; private set; }
946 
947  [Serialize(float.PositiveInfinity, IsPropertySaveable.No, description: "The max distance at which the bots are allowed to target the items. Defaults to infinity.")]
948  public float AITurretTargetingMaxDistance { get; private set; }
949 
950  [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, taking items from this container is never considered stealing.")]
951  public bool AllowStealingContainedItems { get; private set; }
952 
953  [Serialize("255,255,255,255", IsPropertySaveable.No, description: "Used in circuit box to set the color of the nodes.")]
954  public Color SignalComponentColor { get; private set; }
955 
956  [Serialize(false, IsPropertySaveable.No, description: "If enabled, the player is unable to open the middle click menu when this item is selected.")]
957  public bool DisableCommandMenuWhenSelected { get; set; }
958 
959  protected override Identifier DetermineIdentifier(XElement element)
960  {
961  Identifier identifier = base.DetermineIdentifier(element);
962  string originalName = element.GetAttributeString("name", "");
963  if (identifier.IsEmpty && !string.IsNullOrEmpty(originalName))
964  {
965  string categoryStr = element.GetAttributeString("category", "Misc");
966  if (Enum.TryParse(categoryStr, true, out MapEntityCategory category) && category.HasFlag(MapEntityCategory.Legacy))
967  {
968  identifier = GenerateLegacyIdentifier(originalName);
969  }
970  }
971  return identifier;
972  }
973 
974  public static Identifier GenerateLegacyIdentifier(string name)
975  {
976  return ($"legacyitem_{name.Replace(" ", "")}").ToIdentifier();
977  }
978 
979  public ItemPrefab(ContentXElement element, ItemFile file) : base(element, file)
980  {
981  originalElement = element;
982  ConfigElement = element;
983 
984  OriginalName = element.GetAttributeString("name", "");
985  name = OriginalName;
986 
987  VariantOf = element.VariantOf();
988 
989  if (!VariantOf.IsEmpty) { return; } //don't even attempt to read the XML until the PrefabCollection readies up the parent to inherit from
990 
991  ParseConfigElement(variantOf: null);
992  }
993 
994  private string GetTexturePath(ContentXElement subElement, ItemPrefab variantOf)
995  => subElement.DoesAttributeReferenceFileNameAlone("texture")
996  ? Path.GetDirectoryName(variantOf?.ContentFile.Path ?? ContentFile.Path)
997  : "";
998 
999  private void ParseConfigElement(ItemPrefab variantOf)
1000  {
1001  string categoryStr = ConfigElement.GetAttributeString("category", "Misc");
1002  this.category = Enum.TryParse(categoryStr, true, out MapEntityCategory category)
1003  ? category
1004  : MapEntityCategory.Misc;
1005 
1006  //nameidentifier can be used to make multiple items use the same names and descriptions
1007  Identifier nameIdentifier = ConfigElement.GetAttributeIdentifier("nameidentifier", "");
1008 
1009  //only used if the item doesn't have a name/description defined in the currently selected language
1010  string fallbackNameIdentifier = ConfigElement.GetAttributeString("fallbacknameidentifier", "");
1011 
1012  name = TextManager.Get(nameIdentifier.IsEmpty
1013  ? $"EntityName.{Identifier}"
1014  : $"EntityName.{nameIdentifier}",
1015  $"EntityName.{fallbackNameIdentifier}");
1016  if (!string.IsNullOrEmpty(OriginalName))
1017  {
1018  name = name.Fallback(OriginalName);
1019  }
1020 
1021  if (category == MapEntityCategory.Wrecked)
1022  {
1023  name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name);
1024  }
1025 
1027 
1028  this.aliases =
1029  (ConfigElement.GetAttributeStringArray("aliases", null, convertToLowerInvariant: true) ??
1030  ConfigElement.GetAttributeStringArray("Aliases", Array.Empty<string>(), convertToLowerInvariant: true))
1031  .ToImmutableHashSet()
1032  .Add(OriginalName.ToLowerInvariant());
1033 
1034  var triggers = new List<Rectangle>();
1035  var deconstructItems = new List<DeconstructItem>();
1036  var fabricationRecipes = new Dictionary<uint, FabricationRecipe>();
1037  var treatmentSuitability = new Dictionary<Identifier, float>();
1038  var storePrices = new Dictionary<Identifier, PriceInfo>();
1039  var preferredContainers = new List<PreferredContainer>();
1040  DeconstructTime = 1.0f;
1041 
1042  if (ConfigElement.GetAttribute("allowasextracargo") != null)
1043  {
1044  AllowAsExtraCargo = ConfigElement.GetAttributeBool("allowasextracargo", false);
1045  }
1046 
1047  this.tags = ConfigElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableHashSet();
1048  if (!Tags.Any())
1049  {
1050  this.tags = ConfigElement.GetAttributeIdentifierArray("Tags", Array.Empty<Identifier>()).ToImmutableHashSet();
1051  }
1052 
1053  if (ConfigElement.GetAttribute("cargocontainername") != null)
1054  {
1055  DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name.",
1056  contentPackage: ConfigElement.ContentPackage);
1057  }
1058 
1059  SerializableProperty.DeserializeProperties(this, ConfigElement);
1060 
1062  var skillRequirementHints = new List<SkillRequirementHint>();
1063  foreach (var skillRequirementHintElement in ConfigElement.GetChildElements("SkillRequirementHint"))
1064  {
1065  skillRequirementHints.Add(new SkillRequirementHint(skillRequirementHintElement));
1066  }
1067  if (skillRequirementHints.Any())
1068  {
1069  SkillRequirementHints = skillRequirementHints.ToImmutableArray();
1070  }
1071 
1072  var allowDroppingOnSwapWith = ConfigElement.GetAttributeIdentifierArray("allowdroppingonswapwith", Array.Empty<Identifier>());
1073  AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet();
1074  AllowDroppingOnSwap = allowDroppingOnSwapWith.Any();
1075 
1076  var levelCommonness = new Dictionary<Identifier, CommonnessInfo>();
1077  var levelQuantity = new Dictionary<Identifier, FixedQuantityResourceInfo>();
1078 
1079  List<FabricationRecipe> loadedRecipes = new List<FabricationRecipe>();
1080  foreach (ContentXElement subElement in ConfigElement.Elements())
1081  {
1082  switch (subElement.Name.ToString().ToLowerInvariant())
1083  {
1084  case "sprite":
1085  string spriteFolder = GetTexturePath(subElement, variantOf);
1086 
1087  canSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
1088  canSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
1089 
1090  sprite = new Sprite(subElement, spriteFolder, lazyLoad: true);
1091  if (subElement.GetAttribute("sourcerect") == null &&
1092  subElement.GetAttribute("sheetindex") == null)
1093  {
1094  DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!",
1095  contentPackage: ConfigElement.ContentPackage);
1096  }
1097  Size = Sprite.size;
1098 
1099  if (subElement.GetAttribute("name") == null && !Name.IsNullOrWhiteSpace())
1100  {
1101  Sprite.Name = Name.Value;
1102  }
1104  break;
1105  case "price":
1106  if (subElement.GetAttribute("baseprice") != null)
1107  {
1108  foreach (var priceInfo in PriceInfo.CreatePriceInfos(subElement, out defaultPrice))
1109  {
1110  if (priceInfo.StoreIdentifier.IsEmpty) { continue; }
1111  if (storePrices.ContainsKey(priceInfo.StoreIdentifier))
1112  {
1113  DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the store \"{priceInfo.StoreIdentifier}\" defined more than once.",
1114  ContentPackage);
1115  storePrices[priceInfo.StoreIdentifier] = priceInfo;
1116  }
1117  else
1118  {
1119  storePrices.Add(priceInfo.StoreIdentifier, priceInfo);
1120  }
1121  }
1122  }
1123  else if (subElement.GetAttribute("buyprice") != null && subElement.GetAttributeIdentifier("locationtype", "") is { IsEmpty: false } locationType) // Backwards compatibility
1124  {
1125  if (storePrices.ContainsKey(locationType))
1126  {
1127  DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the location type \"{locationType}\" defined more than once.",
1128  ContentPackage);
1129  storePrices[locationType] = new PriceInfo(subElement);
1130  }
1131  else
1132  {
1133  storePrices.Add(locationType, new PriceInfo(subElement));
1134  }
1135  }
1136  break;
1137  case "deconstruct":
1138  DeconstructTime = subElement.GetAttributeFloat("time", 1.0f);
1139  AllowDeconstruct = true;
1140  RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false);
1141  RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1);
1142  foreach (XElement itemElement in subElement.Elements())
1143  {
1144  if (itemElement.Attribute("name") != null)
1145  {
1146  DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items.",
1147  contentPackage: ConfigElement.ContentPackage);
1148  continue;
1149  }
1150  var deconstructItem = new DeconstructItem(itemElement, Identifier);
1151  if (deconstructItem.ItemIdentifier.IsEmpty)
1152  {
1153  DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier.",
1154  contentPackage: ConfigElement.ContentPackage);
1155  continue;
1156  }
1157  deconstructItems.Add(deconstructItem);
1158  }
1159  RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, deconstructItems.Count);
1160  break;
1161  case "fabricate":
1162  case "fabricable":
1163  case "fabricableitem":
1164  var newRecipe = new FabricationRecipe(subElement, Identifier);
1165  if (fabricationRecipes.TryGetValue(newRecipe.RecipeHash, out var prevRecipe))
1166  {
1167  //the errors below may be caused by a mod overriding a base item instead of this one, log the package of the base item in that case
1168  var packageToLog =
1169  (variantOf.ContentPackage != null && variantOf.ContentPackage != ContentPackageManager.VanillaCorePackage) ?
1170  variantOf.ContentPackage :
1172 
1173  int prevRecipeIndex = loadedRecipes.IndexOf(prevRecipe);
1174  DebugConsole.AddWarning(
1175  $"Error in item prefab \"{ToString()}\": " +
1176  $"Fabrication recipe #{loadedRecipes.Count + 1} has the same hash as recipe #{prevRecipeIndex + 1}. This is most likely caused by identical, duplicate recipes. " +
1177  $"This will cause issues with fabrication.",
1178  contentPackage: packageToLog);
1179  }
1180  else
1181  {
1182  fabricationRecipes.Add(newRecipe.RecipeHash, newRecipe);
1183  }
1184  loadedRecipes.Add(newRecipe);
1185  break;
1186  case "preferredcontainer":
1187  var preferredContainer = new PreferredContainer(subElement);
1188  if (preferredContainer.Primary.Count == 0 && preferredContainer.Secondary.Count == 0)
1189  {
1190  //it's ok for variants to clear the primary and secondary containers to disable the PreferredContainer element
1191  if (variantOf == null)
1192  {
1193  DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement}).",
1194  contentPackage: ConfigElement.ContentPackage);
1195  }
1196  }
1197  else
1198  {
1199  preferredContainers.Add(preferredContainer);
1200  }
1201  break;
1202  case "swappableitem":
1203  SwappableItem = new SwappableItem(subElement);
1204  break;
1205  case "trigger":
1206  Rectangle trigger = new Rectangle(0, 0, 10, 10)
1207  {
1208  X = subElement.GetAttributeInt("x", 0),
1209  Y = subElement.GetAttributeInt("y", 0),
1210  Width = subElement.GetAttributeInt("width", 0),
1211  Height = subElement.GetAttributeInt("height", 0)
1212  };
1213 
1214  triggers.Add(trigger);
1215 
1216  break;
1217  case "levelresource":
1218  foreach (XElement levelCommonnessElement in subElement.GetChildElements("commonness"))
1219  {
1220  Identifier levelName = levelCommonnessElement.GetAttributeIdentifier("leveltype", "");
1221  if (!levelCommonnessElement.GetAttributeBool("fixedquantity", false))
1222  {
1223  if (!levelCommonness.ContainsKey(levelName))
1224  {
1225  levelCommonness.Add(levelName, new CommonnessInfo(levelCommonnessElement));
1226  }
1227  }
1228  else
1229  {
1230  if (!levelQuantity.ContainsKey(levelName))
1231  {
1232  levelQuantity.Add(levelName, new FixedQuantityResourceInfo(
1233  levelCommonnessElement.GetAttributeInt("clusterquantity", 0),
1234  levelCommonnessElement.GetAttributeInt("clustersize", 0),
1235  levelCommonnessElement.GetAttributeBool("isislandspecific", false),
1236  levelCommonnessElement.GetAttributeBool("allowatstart", true)));
1237  }
1238  }
1239  }
1240  break;
1241  case "suitabletreatment":
1242  if (subElement.GetAttribute("name") != null)
1243  {
1244  DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names.",
1245  contentPackage: ConfigElement.ContentPackage);
1246  }
1247  Identifier treatmentIdentifier = subElement.GetAttributeIdentifier("identifier", subElement.GetAttributeIdentifier("type", Identifier.Empty));
1248  float suitability = subElement.GetAttributeFloat("suitability", 0.0f);
1249  treatmentSuitability.Add(treatmentIdentifier, suitability);
1250  break;
1251  }
1252  }
1253 
1255 
1256 #if CLIENT
1257  ParseSubElementsClient(ConfigElement, variantOf);
1258 #endif
1259 
1260  this.Triggers = triggers.ToImmutableArray();
1261  this.DeconstructItems = deconstructItems.ToImmutableArray();
1262  this.FabricationRecipes = fabricationRecipes.ToImmutableDictionary();
1263  this.treatmentSuitability = treatmentSuitability.ToImmutableDictionary();
1264  StorePrices = storePrices.ToImmutableDictionary();
1265  this.PreferredContainers = preferredContainers.ToImmutableArray();
1266  this.LevelCommonness = levelCommonness.ToImmutableDictionary();
1267  this.LevelQuantity = levelQuantity.ToImmutableDictionary();
1268 
1269  // Backwards compatibility
1270  if (storePrices.Any())
1271  {
1272  defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false);
1273  }
1274 
1276 
1277  //backwards compatibility
1278  if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase))
1279  {
1280  this.category = MapEntityCategory.Wrecked;
1281  Subcategory = "Thalamus";
1282  }
1283 
1284  if (Sprite == null)
1285  {
1286  DebugConsole.ThrowError($"Item \"{ToString()}\" has no sprite!", contentPackage: ConfigElement.ContentPackage);
1287 #if SERVER
1288  this.sprite = new Sprite("", Vector2.Zero);
1289  this.sprite.SourceRect = new Rectangle(0, 0, 32, 32);
1290 #else
1291  this.sprite = new Sprite(TextureLoader.PlaceHolderTexture, null, null)
1292  {
1293  Origin = TextureLoader.PlaceHolderTexture.Bounds.Size.ToVector2() / 2
1294  };
1295 #endif
1296  Size = Sprite.size;
1298  }
1299 
1300  if (Identifier == Identifier.Empty)
1301  {
1302  DebugConsole.ThrowError(
1303  $"Item prefab \"{ToString()}\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading.",
1304  contentPackage: ConfigElement.ContentPackage);
1305  }
1306 
1307 #if DEBUG
1308  if (!Category.HasFlag(MapEntityCategory.Legacy) && !HideInMenus)
1309  {
1310  if (!string.IsNullOrEmpty(OriginalName))
1311  {
1312  DebugConsole.AddWarning($"Item \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages.",
1313  ContentPackage);
1314  }
1315  }
1316 #endif
1317 
1318  this.allowedLinks = ConfigElement.GetAttributeIdentifierArray("allowedlinks", Array.Empty<Identifier>()).ToImmutableHashSet();
1319 
1321  nameof(GrabWhenSelected),
1322  ConfigElement.GetChildElement(nameof(ItemContainer)) != null &&
1323  ConfigElement.GetChildElement("Body") == null);
1324  }
1325 
1327  {
1328  CommonnessInfo? levelCommonnessInfo = GetValueOrNull(level.GenerationParams.Identifier);
1329  CommonnessInfo? biomeCommonnessInfo = GetValueOrNull(level.LevelData.Biome.Identifier);
1330  CommonnessInfo? defaultCommonnessInfo = GetValueOrNull(Identifier.Empty);
1331 
1332  if (levelCommonnessInfo.HasValue)
1333  {
1334  return levelCommonnessInfo?.WithInheritedCommonness(biomeCommonnessInfo, defaultCommonnessInfo);
1335  }
1336  else if (biomeCommonnessInfo.HasValue)
1337  {
1338  return biomeCommonnessInfo?.WithInheritedCommonness(defaultCommonnessInfo);
1339  }
1340  else if (defaultCommonnessInfo.HasValue)
1341  {
1342  return defaultCommonnessInfo;
1343  }
1344 
1345  return null;
1346 
1347  CommonnessInfo? GetValueOrNull(Identifier identifier)
1348  {
1349  if (LevelCommonness.TryGetValue(identifier, out CommonnessInfo info))
1350  {
1351  return info;
1352  }
1353  else
1354  {
1355  return null;
1356  }
1357  }
1358  }
1359 
1360  public float GetTreatmentSuitability(Identifier treatmentIdentifier)
1361  {
1362  return treatmentSuitability.TryGetValue(treatmentIdentifier, out float suitability) ? suitability : 0.0f;
1363  }
1364 
1365  #region Pricing
1366 
1368  {
1369  if (store == null)
1370  {
1371  string message = $"Tried to get price info for \"{Identifier}\" with a null store parameter!\n{Environment.StackTrace.CleanupStackTrace()}";
1372 #if DEBUG
1373  DebugConsole.LogError(message, contentPackage: ContentPackage);
1374 #else
1375  DebugConsole.AddWarning(message, ContentPackage);
1376  GameAnalyticsManager.AddErrorEventOnce("ItemPrefab.GetPriceInfo:StoreParameterNull", GameAnalyticsManager.ErrorSeverity.Error, message);
1377 #endif
1378  return null;
1379  }
1380  else if (!store.Identifier.IsEmpty && StorePrices != null && StorePrices.TryGetValue(store.Identifier, out var storePriceInfo))
1381  {
1382  return storePriceInfo;
1383  }
1384  else
1385  {
1386  return DefaultPrice;
1387  }
1388  }
1389 
1390  public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo)
1391  {
1392  priceInfo = GetPriceInfo(store);
1393  return
1394  priceInfo is { CanBeBought: true } &&
1395  (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty &&
1396  (!priceInfo.MinReputation.Any() || priceInfo.MinReputation.Any(p => store?.Location.Faction?.Prefab.Identifier == p.Key || store?.Location.SecondaryFaction?.Prefab.Identifier == p.Key));
1397  }
1398 
1399  public bool CanBeBoughtFrom(Location location)
1400  {
1401  if (location?.Stores == null) { return false; }
1402  foreach (var store in location.Stores)
1403  {
1404  var priceInfo = GetPriceInfo(store.Value);
1405  if (priceInfo == null) { continue; }
1406  if (!priceInfo.CanBeBought) { continue; }
1407  if (location.LevelData.Difficulty < priceInfo.MinLevelDifficulty) { continue; }
1408  if (priceInfo.MinReputation.Any())
1409  {
1410  if (!priceInfo.MinReputation.Any(p =>
1411  location?.Faction?.Prefab.Identifier == p.Key ||
1412  location?.SecondaryFaction?.Prefab.Identifier == p.Key))
1413  {
1414  continue;
1415  }
1416  }
1417  return true;
1418  }
1419  return false;
1420  }
1421 
1422  public int? GetMinPrice()
1423  {
1424  int? minPrice = null;
1425  if (StorePrices != null && StorePrices.Any())
1426  {
1427  minPrice = StorePrices.Values.Min(p => p.Price);
1428  }
1429  if (minPrice.HasValue)
1430  {
1431  if (DefaultPrice != null)
1432  {
1433  return minPrice < DefaultPrice.Price ? minPrice : DefaultPrice.Price;
1434  }
1435  else
1436  {
1437  return minPrice.Value;
1438  }
1439  }
1440  else
1441  {
1442  return DefaultPrice?.Price;
1443  }
1444  }
1445 
1446  public ImmutableDictionary<Identifier, PriceInfo> GetBuyPricesUnder(int maxCost = 0)
1447  {
1448  var prices = new Dictionary<Identifier, PriceInfo>();
1449  if (StorePrices != null)
1450  {
1451  foreach (var storePrice in StorePrices)
1452  {
1453  var priceInfo = storePrice.Value;
1454  if (priceInfo == null)
1455  {
1456  continue;
1457  }
1458  if (!priceInfo.CanBeBought)
1459  {
1460  continue;
1461  }
1462  if (priceInfo.Price < maxCost || maxCost == 0)
1463  {
1464  prices.Add(storePrice.Key, priceInfo);
1465  }
1466  }
1467  }
1468  return prices.ToImmutableDictionary();
1469  }
1470 
1471  public ImmutableDictionary<Identifier, PriceInfo> GetSellPricesOver(int minCost = 0, bool sellingImportant = true)
1472  {
1473  var prices = new Dictionary<Identifier, PriceInfo>();
1474  if (!CanBeSold && sellingImportant)
1475  {
1476  return prices.ToImmutableDictionary();
1477  }
1478  foreach (var storePrice in StorePrices)
1479  {
1480  var priceInfo = storePrice.Value;
1481  if (priceInfo == null)
1482  {
1483  continue;
1484  }
1485  if (priceInfo.Price > minCost)
1486  {
1487  prices.Add(storePrice.Key, priceInfo);
1488  }
1489  }
1490  return prices.ToImmutableDictionary();
1491  }
1492 
1493  #endregion
1494 
1495  public static ItemPrefab Find(string name, Identifier identifier)
1496  {
1497  if (string.IsNullOrEmpty(name) && identifier.IsEmpty)
1498  {
1499  throw new ArgumentException("Both name and identifier cannot be null.");
1500  }
1501 
1502  if (identifier.IsEmpty)
1503  {
1504  //legacy support
1505  identifier = GenerateLegacyIdentifier(name);
1506  }
1507  Prefabs.TryGet(identifier, out ItemPrefab prefab);
1508 
1509  //not found, see if we can find a prefab with a matching alias
1510  if (prefab == null && !string.IsNullOrEmpty(name))
1511  {
1512  string lowerCaseName = name.ToLowerInvariant();
1513  prefab = Prefabs.Find(me => me.Aliases != null && me.Aliases.Contains(lowerCaseName));
1514  }
1515  if (prefab == null)
1516  {
1517  prefab = Prefabs.Find(me => me.Aliases != null && me.Aliases.Contains(identifier.Value));
1518  }
1519 
1520  if (prefab == null)
1521  {
1522  DebugConsole.ThrowError($"Error loading item - item prefab \"{name}\" (identifier \"{identifier}\") not found.");
1523  }
1524  return prefab;
1525  }
1526 
1527  public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false, bool checkTransferConditions = false)
1528  {
1529  isPreferencesDefined = PreferredContainers.Any();
1530  isSecondary = false;
1531  if (!isPreferencesDefined) { return true; }
1532  if (PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) &&
1533  IsContainerPreferred(pc.Primary, targetContainer) && (!checkTransferConditions || CanBeTransferred(item.Prefab.Identifier, pc, targetContainer))))
1534  {
1535  return true;
1536  }
1537  isSecondary = true;
1538  return PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, targetContainer));
1539  static bool HasConditionRequirement(PreferredContainer pc) => pc.MinCondition > 0 || pc.MaxCondition < 100;
1540  }
1541 
1542  public bool IsContainerPreferred(Item item, Identifier[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary)
1543  {
1544  isPreferencesDefined = PreferredContainers.Any();
1545  isSecondary = false;
1546  if (!isPreferencesDefined) { return true; }
1547  if (PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, identifiersOrTags)))
1548  {
1549  return true;
1550  }
1551  isSecondary = true;
1552  return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, identifiersOrTags));
1553  }
1554 
1555  private static bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition;
1556  private static bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) =>
1557  pc.AllowTransfersHere && (!pc.TransferOnlyOnePerContainer || targetContainer.Inventory.AllItems.None(i => i.Prefab.Identifier == item));
1558 
1559  public static bool IsContainerPreferred(IEnumerable<Identifier> preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id));
1560  public static bool IsContainerPreferred(IEnumerable<Identifier> preferences, IEnumerable<Identifier> ids) => ids.Any(id => preferences.Contains(id));
1561 
1562  protected override void CreateInstance(Rectangle rect)
1563  {
1564  throw new InvalidOperationException("Can't call ItemPrefab.CreateInstance");
1565  }
1566 
1567  public override void Dispose()
1568  {
1569  Item.RemoveByPrefab(this);
1570  }
1571 
1572  public Identifier VariantOf { get; }
1573  public ItemPrefab ParentPrefab { get; set; }
1574 
1575  public void InheritFrom(ItemPrefab parent)
1576  {
1577  ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement, CheckXML).FromPackage(ConfigElement.ContentPackage);
1578  ParseConfigElement(parent);
1579 
1580  void CheckXML(XElement originalElement, XElement variantElement, XElement result)
1581  {
1582  //if either the parent or the variant are non-vanilla, assume the error is coming from that package
1583  var packageToLog = parent.ContentPackage != GameMain.VanillaContent ? parent.ContentPackage : ContentPackage;
1584 
1585  if (result == null) { return; }
1586  if (result.Name.ToIdentifier() == "RequiredItem" &&
1587  result.Parent?.Name.ToIdentifier() == "Fabricate")
1588  {
1589  int originalAmount = originalElement.GetAttributeInt("amount", 1);
1590  Identifier originalIdentifier = originalElement.GetAttributeIdentifier("identifier", Identifier.Empty);
1591  if (variantElement == null)
1592  {
1593  //if the variant defines some fabrication requirements, we probably don't want to inherit anything extra from the base item?
1594  if (this.originalElement.GetChildElement("Fabricate")?.GetChildElement("RequiredItem") != null)
1595  {
1596  DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " +
1597  $"the item inherits the fabrication requirement of x{originalAmount} \"{originalIdentifier}\" from the base item \"{parent.Identifier}\". " +
1598  $"If this is not intentional, you can use empty <RequiredItem /> elements in the item variant to remove any excess inherited fabrication requirements.",
1599  packageToLog);
1600  }
1601  return;
1602  }
1603 
1604  Identifier resultIdentifier = result.GetAttributeIdentifier("identifier", Identifier.Empty);
1605  if (originalAmount > 1 && variantElement.GetAttribute("amount") == null)
1606  {
1607  DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " +
1608  $"the base item \"{parent.Identifier}\" requires x{originalAmount} \"{originalIdentifier}\" to fabricate. " +
1609  $"The variant only overrides the required item, not the amount, resulting in a requirement of x{originalAmount} \"{resultIdentifier}\". "+
1610  "Specify the amount in the variant to fix this.",
1611  packageToLog);
1612  }
1613  }
1614  if (originalElement?.Name.ToIdentifier() == "Deconstruct" &&
1615  variantElement?.Name.ToIdentifier() == "Deconstruct")
1616  {
1617  if (originalElement.Elements().Any(e => e.Name.ToIdentifier() == "Item") &&
1618  variantElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem"))
1619  {
1620  DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " +
1621  $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. Overriding the base recipe may not work correctly.",
1622  packageToLog);
1623  }
1624  if (variantElement.Elements().Any(e => e.Name.ToIdentifier() == "Item") &&
1625  originalElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem"))
1626  {
1627  DebugConsole.AddWarning($"Potential error in item \"{parent.Identifier}\": " +
1628  $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. The item variant \"{Identifier}\" may not override the base recipe correctly.",
1629  packageToLog);
1630  }
1631  }
1632  }
1633  }
1634 
1641  {
1642  if (ParentPrefab != null &&
1643  ParentPrefab.ContentPackage != ContentPackageManager.VanillaCorePackage)
1644  {
1646  }
1647  return ContentPackage;
1648  }
1649 
1650  public override string ToString()
1651  {
1652  return $"{Name} (identifier: {Identifier})";
1653  }
1654  }
1655 }
Base class for content file types, which are loaded from filelist.xml via reflection....
Definition: ContentFile.cs:23
readonly ContentPath Path
Definition: ContentFile.cs:137
string? GetAttributeString(string key, string? def)
Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys)
float GetAttributeFloat(string key, float def)
ContentPackage? ContentPackage
IEnumerable< ContentXElement > GetChildElements(string name)
IEnumerable< ContentXElement > Elements()
Vector2 GetAttributeVector2(string key, in Vector2 def)
ContentXElement? GetChildElement(string name)
bool GetAttributeBool(string key, bool def)
int GetAttributeInt(string key, int def)
string?[] GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant=false)
XAttribute? GetAttribute(string name)
Identifier GetAttributeIdentifier(string key, string def)
RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader)
RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem)
abstract bool MatchesItem(Item item)
RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem)
readonly Identifier DefaultItem
Used only when there's multiple optional items.
readonly ImmutableArray< Identifier > SuitableFabricatorIdentifiers
FabricationRecipe(ContentXElement element, Identifier itemPrefab)
readonly ImmutableArray< RequiredItem > RequiredItems
readonly int FabricationLimitMin
How many of this item the fabricator can create (< 0 = unlimited)
FactionPrefab Prefab
Definition: Factions.cs:18
static GameSession?? GameSession
Definition: GameMain.cs:88
static ContentPackage VanillaContent
Definition: GameMain.cs:84
static void RemoveByPrefab(ItemPrefab prefab)
static readonly PrefabCollection< ItemPrefab > Prefabs
override ImmutableHashSet< Identifier > AllowedLinks
bool CanBeSold
Any item with a Price element in the definition can be sold everywhere.
ImmutableHashSet< Identifier > AllowDroppingOnSwapWith
float GetTreatmentSuitability(Identifier treatmentIdentifier)
static ItemPrefab Find(string name, Identifier identifier)
ImmutableArray< SkillRequirementHint > SkillRequirementHints
ImmutableDictionary< Identifier, FixedQuantityResourceInfo > LevelQuantity
ImmutableArray< PreferredContainer > PreferredContainers
static Identifier GenerateLegacyIdentifier(string name)
ItemPrefab(ContentXElement element, ItemFile file)
ImmutableDictionary< Identifier, PriceInfo > GetBuyPricesUnder(int maxCost=0)
override Identifier DetermineIdentifier(XElement element)
bool IsOverride
Is this prefab overriding a prefab in another content package
ImmutableArray< Rectangle > Triggers
Defines areas where the item can be interacted with. If RequireBodyInsideTrigger is set to true,...
PriceInfo GetPriceInfo(Location.StoreInfo store)
ImmutableDictionary< Identifier, PriceInfo > GetSellPricesOver(int minCost=0, bool sellingImportant=true)
ImmutableDictionary< uint, FabricationRecipe > FabricationRecipes
bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo)
ContentPackage GetParentModPackageOrThisPackage()
If the base prefab this one is a variant of is defined in a non-vanilla package, returns that non-van...
override ImmutableHashSet< string > Aliases
ImmutableArray< DeconstructItem > DeconstructItems
bool IsContainerPreferred(Item item, Identifier[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary)
override ImmutableHashSet< Identifier > Tags
static bool IsContainerPreferred(IEnumerable< Identifier > preferences, ItemContainer c)
bool? AllowAsExtraCargo
Can the item be chosen as extra cargo in multiplayer. If not set, the item is available if it can be ...
static bool IsContainerPreferred(IEnumerable< Identifier > preferences, IEnumerable< Identifier > ids)
bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement=false, bool checkTransferConditions=false)
static LocalizedString TryCreateName(ItemPrefab prefab, XElement element)
readonly Biome Biome
Definition: LevelData.cs:25
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
override bool Equals(object? obj)
Definition: Md5Hash.cs:147
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
Identifier EntityIdentifier
Identifier of the Map Entity so that we can link the sprite to its owner.
List<(Identifier requiredTag, Identifier swapTo)> ConnectedItemsToSwap
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180
DeconstructItem(XElement element, Identifier parentDebugName)
CommonnessInfo WithInheritedCommonness(CommonnessInfo? parentInfo)
CommonnessInfo WithInheritedCommonness(params CommonnessInfo?[] parentInfos)
CommonnessInfo(float commonness, float? abyssCommonness, float? caveCommonness)
FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific, bool allowAtStart)
LocalizedString GetFormattedText(int skillLevel, string levelColorTag)