using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Barotrauma.Extensions; using System.Security.Cryptography; using System.Xml.Linq; namespace Barotrauma { readonly struct DeconstructItem { public readonly Identifier ItemIdentifier; //number of items to output public readonly int Amount; //minCondition does <= check, meaning that below or equal to min condition will be skipped. public readonly float MinCondition; //maxCondition does > check, meaning that above this max the deconstruct item will be skipped. public readonly float MaxCondition; //Condition of item on creation public readonly float OutConditionMin, OutConditionMax; //should the condition of the deconstructed item be copied to the output items public readonly bool CopyCondition; //tag/identifier of the deconstructor(s) that can be used to deconstruct the item into this public readonly string[] RequiredDeconstructor; //tag/identifier of other item(s) that that need to be present in the deconstructor to deconstruct the item into this public readonly string[] RequiredOtherItem; //text to display on the deconstructor's activate button when this output is available public readonly string ActivateButtonText; public readonly string InfoText; public readonly string InfoTextOnOtherItemMissing; public readonly float Commonness; public DeconstructItem(XElement element, Identifier parentDebugName) { ItemIdentifier = element.GetAttributeIdentifier("identifier", ""); Amount = element.GetAttributeInt("amount", 1); MinCondition = element.GetAttributeFloat("mincondition", -0.1f); MaxCondition = element.GetAttributeFloat("maxcondition", 1.0f); OutConditionMin = element.GetAttributeFloat("outconditionmin", element.GetAttributeFloat("outcondition", 1.0f)); OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f)); CopyCondition = element.GetAttributeBool("copycondition", false); Commonness = element.GetAttributeFloat("commonness", 1.0f); RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", element.Parent?.GetAttributeStringArray("requireddeconstructor", Array.Empty()) ?? Array.Empty()); RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", Array.Empty()); ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty); InfoText = element.GetAttributeString("infotext", string.Empty); InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty); } public bool IsValidDeconstructor(Item deconstructor) { return RequiredDeconstructor.Length == 0 || RequiredDeconstructor.Any(r => deconstructor.HasTag(r) || deconstructor.Prefab.Identifier == r); } } class FabricationRecipe { public abstract class RequiredItem { public abstract IEnumerable ItemPrefabs { get; } public abstract UInt32 UintIdentifier { get; } public abstract bool MatchesItem(Item item); public abstract ItemPrefab FirstMatchingPrefab { get; } public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition) { Amount = amount; MinCondition = minCondition; MaxCondition = maxCondition; UseCondition = useCondition; } public readonly int Amount; public readonly float MinCondition; public readonly float MaxCondition; public readonly bool UseCondition; public bool IsConditionSuitable(float conditionPercentage) { float normalizedCondition = conditionPercentage / 100.0f; if (MathUtils.NearlyEqual(normalizedCondition, MinCondition) || MathUtils.NearlyEqual(normalizedCondition, MaxCondition)) { return true; } else if (normalizedCondition >= MinCondition && normalizedCondition <= MaxCondition) { return true; } return false; } } public class RequiredItemByIdentifier : RequiredItem { public readonly Identifier ItemPrefabIdentifier; public ItemPrefab ItemPrefab => ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab : MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab; public override UInt32 UintIdentifier { get; } public override IEnumerable ItemPrefabs => ItemPrefab == null ? Enumerable.Empty() : ItemPrefab.ToEnumerable(); public override ItemPrefab FirstMatchingPrefab => ItemPrefab; public override bool MatchesItem(Item item) { return item?.Prefab.Identifier == ItemPrefabIdentifier; } public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition) { ItemPrefabIdentifier = itemPrefab; using MD5 md5 = MD5.Create(); UintIdentifier = ToolBox.IdentifierToUint32Hash(itemPrefab, md5); } public override string ToString() { return $"{base.ToString()} ({ItemPrefabIdentifier})"; } } public class RequiredItemByTag : RequiredItem { public readonly Identifier Tag; public override UInt32 UintIdentifier { get; } public override IEnumerable ItemPrefabs => ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag)); public override ItemPrefab FirstMatchingPrefab => ItemPrefab.Prefabs.FirstOrDefault(p => p.Tags.Contains(Tag)); public override bool MatchesItem(Item item) { if (item == null) { return false; } return item.HasTag(Tag); } public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition) { Tag = tag; using MD5 md5 = MD5.Create(); UintIdentifier = ToolBox.IdentifierToUint32Hash(tag, md5); } public override string ToString() { return $"{base.ToString()} ({Tag})"; } } public readonly Identifier TargetItemPrefabIdentifier; public ItemPrefab TargetItem => ItemPrefab.Prefabs[TargetItemPrefabIdentifier]; private readonly Lazy displayName; public LocalizedString DisplayName => ItemPrefab.Prefabs.ContainsKey(TargetItemPrefabIdentifier) ? displayName.Value : ""; public readonly ImmutableArray RequiredItems; public readonly ImmutableArray SuitableFabricatorIdentifiers; public readonly float RequiredTime; public readonly int RequiredMoney; public readonly bool RequiresRecipe; public readonly float OutCondition; //Percentage-based from 0 to 1 public readonly ImmutableArray RequiredSkills; public readonly uint RecipeHash; public readonly int Amount; public readonly int? Quality; /// /// How many of this item the fabricator can create (< 0 = unlimited) /// public readonly int FabricationLimitMin, FabricationLimitMax; public FabricationRecipe(XElement element, Identifier itemPrefab) { TargetItemPrefabIdentifier = itemPrefab; var displayNameIdentifier = element.GetAttributeIdentifier("displayname", ""); displayName = new Lazy(() => displayNameIdentifier.IsEmpty ? TargetItem.Name : TextManager.GetWithVariable($"DisplayName.{displayNameIdentifier}", "[itemname]", TargetItem.Name)); SuitableFabricatorIdentifiers = element.GetAttributeIdentifierArray("suitablefabricators", Array.Empty()).ToImmutableArray(); var requiredSkills = new List(); RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f); RequiredMoney = element.GetAttributeInt("requiredmoney", 0); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); if (OutCondition > 1.0f) { DebugConsole.AddWarning($"Error in \"{itemPrefab}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100})."); } var requiredItems = new List(); RequiresRecipe = element.GetAttributeBool("requiresrecipe", false); Amount = element.GetAttributeInt("amount", 1); int limitDefault = element.GetAttributeInt("fabricationlimit", -1); FabricationLimitMin = element.GetAttributeInt(nameof(FabricationLimitMin), limitDefault); FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault); if (element.GetAttribute(nameof(Quality)) != null) { Quality = element.GetAttributeInt(nameof(Quality), 0); } foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "requiredskill": if (subElement.Attribute("name") != null) { DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! Use skill identifiers instead of names."); continue; } requiredSkills.Add(new Skill( subElement.GetAttributeIdentifier("identifier", ""), subElement.GetAttributeInt("level", 0))); break; case "item": case "requireditem": Identifier requiredItemIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); Identifier requiredItemTag = subElement.GetAttributeIdentifier("tag", Identifier.Empty); if (requiredItemIdentifier == Identifier.Empty && requiredItemTag == Identifier.Empty) { DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! One of the required items has no identifier or tag."); continue; } float minCondition = subElement.GetAttributeFloat("mincondition", 1.0f); float maxCondition = subElement.GetAttributeFloat("maxcondition", 1.0f); //Substract mincondition from required item's condition or delete it regardless? bool useCondition = subElement.GetAttributeBool("usecondition", true); int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1)); if (requiredItemIdentifier != Identifier.Empty) { var existing = requiredItems.FindIndex(r => r is RequiredItemByIdentifier ri && ri.ItemPrefabIdentifier == requiredItemIdentifier && MathUtils.NearlyEqual(r.MinCondition, minCondition) && MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); if (existing >= 0) { amount += requiredItems[existing].Amount; requiredItems.RemoveAt(existing); } requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition)); } else { var existing = requiredItems.FindIndex(r => r is RequiredItemByTag rt && rt.Tag == requiredItemTag && MathUtils.NearlyEqual(r.MinCondition, minCondition) && MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); if (existing >= 0) { amount += requiredItems[existing].Amount; requiredItems.RemoveAt(existing); } requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition)); } break; } } this.RequiredSkills = requiredSkills.ToImmutableArray(); this.RequiredItems = requiredItems.ToImmutableArray(); RecipeHash = GenerateHash(); } private uint GenerateHash() { using var md5 = MD5.Create(); uint outputId = ToolBox.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5); var requiredItems = string.Join(':', RequiredItems .Select(i => i.UintIdentifier) .Select(i => string.Join(',', i))); var requiredSkills = string.Join(':', RequiredSkills.Select(s => $"{s.Identifier}:{s.Level}")); uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{requiredItems}|{requiredSkills}", md5); if (retVal == 0) { retVal = 1; } return retVal; } } class PreferredContainer { public readonly ImmutableHashSet Primary; public readonly ImmutableHashSet Secondary; public readonly float SpawnProbability; public readonly float MaxCondition; public readonly float MinCondition; public readonly int MinAmount; public readonly int MaxAmount; // Overrides min and max, if defined. public readonly int Amount; public readonly bool CampaignOnly; public readonly bool NotCampaign; public readonly bool TransferOnlyOnePerContainer; public readonly bool AllowTransfersHere = true; public PreferredContainer(XElement element) { Primary = XMLExtensions.GetAttributeIdentifierArray(element, "primary", Array.Empty()).ToImmutableHashSet(); Secondary = XMLExtensions.GetAttributeIdentifierArray(element, "secondary", Array.Empty()).ToImmutableHashSet(); SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f); MinAmount = element.GetAttributeInt("minamount", 0); MaxAmount = Math.Max(MinAmount, element.GetAttributeInt("maxamount", 0)); Amount = element.GetAttributeInt("amount", 0); MaxCondition = element.GetAttributeFloat("maxcondition", 100f); MinCondition = element.GetAttributeFloat("mincondition", 0f); CampaignOnly = element.GetAttributeBool("campaignonly", CampaignOnly); NotCampaign = element.GetAttributeBool("notcampaign", NotCampaign); TransferOnlyOnePerContainer = element.GetAttributeBool("TransferOnlyOnePerContainer", TransferOnlyOnePerContainer); AllowTransfersHere = element.GetAttributeBool("AllowTransfersHere", AllowTransfersHere); if (element.GetAttribute("spawnprobability") == null) { //if spawn probability is not defined but amount is, assume the probability is 1 if (MaxAmount > 0 || Amount > 0) { SpawnProbability = 1.0f; } } else if (element.GetAttribute("minamount") == null && element.GetAttribute("maxamount") == null && element.GetAttribute("amount") == null) { //spawn probability defined but amount isn't, assume amount is 1 MinAmount = MaxAmount = Amount = 1; SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f); } } } class SwappableItem { public int BasePrice { get; } public readonly bool CanBeBought; public readonly Identifier ReplacementOnUninstall; public string SpawnWithId; public string SwapIdentifier; public readonly Vector2 SwapOrigin; public List<(Identifier requiredTag, Identifier swapTo)> ConnectedItemsToSwap = new List<(Identifier requiredTag, Identifier swapTo)>(); public readonly Sprite SchematicSprite; public int GetPrice(Location location = null) { int price = BasePrice; return location?.GetAdjustedMechanicalCost(price) ?? price; } public SwappableItem(ContentXElement element) { BasePrice = Math.Max(element.GetAttributeInt("price", 0), 0); SwapIdentifier = element.GetAttributeString("swapidentifier", string.Empty); CanBeBought = element.GetAttributeBool("canbebought", BasePrice != 0); ReplacementOnUninstall = element.GetAttributeIdentifier("replacementonuninstall", ""); SwapOrigin = element.GetAttributeVector2("origin", Vector2.One); SpawnWithId = element.GetAttributeString("spawnwithid", string.Empty); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "schematicsprite": SchematicSprite = new Sprite(subElement); break; case "swapconnecteditem": ConnectedItemsToSwap.Add( (subElement.GetAttributeIdentifier("tag", ""), subElement.GetAttributeIdentifier("swapto", ""))); break; } } } } partial class ItemPrefab : MapEntityPrefab, IImplementsVariants { public static readonly PrefabCollection Prefabs = new PrefabCollection(); public const float DefaultInteractDistance = 120.0f; //default size public Vector2 Size { get; private set; } private PriceInfo defaultPrice; public PriceInfo DefaultPrice => defaultPrice; private ImmutableDictionary StorePrices { get; set; } public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || (StorePrices != null && StorePrices.Any(p => p.Value.CanBeBought)); /// /// Any item with a Price element in the definition can be sold everywhere. /// public bool CanBeSold => DefaultPrice != null; /// /// Defines areas where the item can be interacted with. If RequireBodyInsideTrigger is set to true, the character /// has to be within the trigger to interact. If it's set to false, having the cursor within the trigger is enough. /// public ImmutableArray Triggers { get; private set; } private ImmutableDictionary treatmentSuitability; /// /// Is this prefab overriding a prefab in another content package /// public bool IsOverride => Prefabs.IsOverride(this); private readonly XElement originalElement; public ContentXElement ConfigElement { get; private set; } public ImmutableArray DeconstructItems { get; private set; } public ImmutableDictionary FabricationRecipes { get; private set; } public float DeconstructTime { get; private set; } public bool AllowDeconstruct { get; private set; } //Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced. public ImmutableArray PreferredContainers { get; private set; } public SwappableItem SwappableItem { get; private set; } public readonly struct CommonnessInfo { public float Commonness { get { return commonness; } } public float AbyssCommonness { get { return abyssCommonness ?? 0.0f; } } public float CaveCommonness { get { return caveCommonness ?? Commonness; } } public bool CanAppear { get { if (Commonness > 0.0f) { return true; } if (AbyssCommonness > 0.0f) { return true; } if (CaveCommonness > 0.0f) { return true; } return false; } } public readonly float commonness; public readonly float? abyssCommonness; public readonly float? caveCommonness; public CommonnessInfo(XElement element) { this.commonness = Math.Max(element?.GetAttributeFloat("commonness", 0.0f) ?? 0.0f, 0.0f); float? abyssCommonness = null; XAttribute abyssCommonnessAttribute = element?.GetAttribute("abysscommonness") ?? element?.GetAttribute("abyss"); if (abyssCommonnessAttribute != null) { abyssCommonness = Math.Max(abyssCommonnessAttribute.GetAttributeFloat(0.0f), 0.0f); } this.abyssCommonness = abyssCommonness; float? caveCommonness = null; XAttribute caveCommonnessAttribute = element?.GetAttribute("cavecommonness") ?? element?.GetAttribute("cave"); if (caveCommonnessAttribute != null) { caveCommonness = Math.Max(caveCommonnessAttribute.GetAttributeFloat(0.0f), 0.0f); } this.caveCommonness = caveCommonness; } public CommonnessInfo(float commonness, float? abyssCommonness, float? caveCommonness) { this.commonness = commonness; this.abyssCommonness = abyssCommonness != null ? (float?)Math.Max(abyssCommonness.Value, 0.0f) : null; this.caveCommonness = caveCommonness != null ? (float?)Math.Max(caveCommonness.Value, 0.0f) : null; } public CommonnessInfo WithInheritedCommonness(CommonnessInfo? parentInfo) { return new CommonnessInfo(commonness, abyssCommonness ?? parentInfo?.abyssCommonness, caveCommonness ?? parentInfo?.caveCommonness); } public CommonnessInfo WithInheritedCommonness(params CommonnessInfo?[] parentInfos) { CommonnessInfo info = this; foreach (var parentInfo in parentInfos) { info = info.WithInheritedCommonness(parentInfo); } return info; } public float GetCommonness(Level.TunnelType tunnelType) { if (tunnelType == Level.TunnelType.Cave) { return CaveCommonness; } else { return Commonness; } } } /// /// How likely it is for the item to spawn in a level of a given type. /// private ImmutableDictionary LevelCommonness { get; set; } public readonly struct FixedQuantityResourceInfo { public readonly int ClusterQuantity; public readonly int ClusterSize; public readonly bool IsIslandSpecific; public readonly bool AllowAtStart; public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific, bool allowAtStart) { ClusterQuantity = clusterQuantity; ClusterSize = clusterSize; IsIslandSpecific = isIslandSpecific; AllowAtStart = allowAtStart; } } public ImmutableDictionary LevelQuantity { get; private set; } private bool canSpriteFlipX; public override bool CanSpriteFlipX => canSpriteFlipX; private bool canSpriteFlipY; public override bool CanSpriteFlipY => canSpriteFlipY; /// /// Can the item be chosen as extra cargo in multiplayer. If not set, the item is available if it can be bought from outposts in the campaign. /// public bool? AllowAsExtraCargo { get; private set; } public bool RandomDeconstructionOutput { get; private set; } public int RandomDeconstructionOutputAmount { get; private set; } private Sprite sprite; public override Sprite Sprite => sprite; public override string OriginalName { get; } private LocalizedString name; public override LocalizedString Name => name; private ImmutableHashSet tags; public override ImmutableHashSet Tags => tags; private ImmutableHashSet allowedLinks; public override ImmutableHashSet AllowedLinks => allowedLinks; private MapEntityCategory category; public override MapEntityCategory Category => category; private ImmutableHashSet aliases; public override ImmutableHashSet Aliases => aliases; //how close the Character has to be to the item to pick it up [Serialize(DefaultInteractDistance, IsPropertySaveable.No)] public float InteractDistance { get; private set; } // this can be used to allow items which are behind other items tp [Serialize(0.0f, IsPropertySaveable.No)] public float InteractPriority { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool InteractThroughWalls { get; private set; } [Serialize(false, IsPropertySaveable.No, description: "Hides the condition bar displayed at the bottom of the inventory slot the item is in.")] public bool HideConditionBar { get; set; } [Serialize(false, IsPropertySaveable.No, description: "Hides the condition displayed in the item's tooltip.")] public bool HideConditionInTooltip { get; set; } //if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item //if false, trigger areas define areas that can be used to highlight the item [Serialize(true, IsPropertySaveable.No)] public bool RequireBodyInsideTrigger { get; private set; } //if true and the item has trigger areas defined, players can only highlight the item when the cursor is on the trigger [Serialize(false, IsPropertySaveable.No)] public bool RequireCursorInsideTrigger { get; private set; } //if true then players can only highlight the item if its targeted for interaction by a campaign event [Serialize(false, IsPropertySaveable.No)] public bool RequireCampaignInteract { get; private set; } //should the camera focus on the item when selected [Serialize(false, IsPropertySaveable.No)] public bool FocusOnSelected { get; private set; } //the amount of "camera offset" when selecting the construction [Serialize(0.0f, IsPropertySaveable.No)] public float OffsetOnSelected { get; private set; } [Serialize(100.0f, IsPropertySaveable.No)] public float Health { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool AllowSellingWhenBroken { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool Indestructible { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool DamagedByExplosions { get; private set; } [Serialize(1f, IsPropertySaveable.No)] public float ExplosionDamageMultiplier { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool DamagedByProjectiles { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool DamagedByMeleeWeapons { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool DamagedByRepairTools { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool DamagedByMonsters { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool FireProof { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool WaterProof { get; private set; } private float impactTolerance; [Serialize(0.0f, IsPropertySaveable.No)] public float ImpactTolerance { get { return impactTolerance; } set { impactTolerance = Math.Max(value, 0.0f); } } [Serialize(0.0f, IsPropertySaveable.No)] public float OnDamagedThreshold { get; set; } [Serialize(0.0f, IsPropertySaveable.No)] public float SonarSize { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool UseInHealthInterface { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool DisableItemUsageWhenSelected { get; private set; } [Serialize("", IsPropertySaveable.No)] public string CargoContainerIdentifier { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool UseContainedSpriteColor { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool UseContainedInventoryIconColor { get; private set; } [Serialize(0.0f, IsPropertySaveable.No)] public float AddedRepairSpeedMultiplier { get; private set; } [Serialize(0.0f, IsPropertySaveable.No)] public float AddedPickingSpeedMultiplier { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool CannotRepairFail { get; private set; } [Serialize(null, IsPropertySaveable.No)] public string EquipConfirmationText { get; set; } [Serialize(true, IsPropertySaveable.No, description: "Can the item be rotated in the submarine editor.")] public bool AllowRotatingInEditor { get; set; } [Serialize(false, IsPropertySaveable.No)] public bool ShowContentsInTooltip { get; private set; } [Serialize(true, IsPropertySaveable.No)] public bool CanFlipX { get; private set; } [Serialize(true, IsPropertySaveable.No)] public bool CanFlipY { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool IsDangerous { get; private set; } private int maxStackSize; [Serialize(1, IsPropertySaveable.No)] public int MaxStackSize { get { return maxStackSize; } private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); } } [Serialize(false, IsPropertySaveable.No)] public bool AllowDroppingOnSwap { get; private set; } public ImmutableHashSet AllowDroppingOnSwapWith { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool DontTransferBetweenSubs { get; private set; } [Serialize(true, IsPropertySaveable.No)] public bool ShowHealthBar { get; private set; } [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.")] public float BotPriority { get; private set; } protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); string originalName = element.GetAttributeString("name", ""); if (identifier.IsEmpty && !string.IsNullOrEmpty(originalName)) { string categoryStr = element.GetAttributeString("category", "Misc"); if (Enum.TryParse(categoryStr, true, out MapEntityCategory category) && category.HasFlag(MapEntityCategory.Legacy)) { identifier = GenerateLegacyIdentifier(originalName); } } return identifier; } public static Identifier GenerateLegacyIdentifier(string name) { return ($"legacyitem_{name.Replace(" ", "")}").ToIdentifier(); } public ItemPrefab(ContentXElement element, ItemFile file) : base(element, file) { originalElement = element; ConfigElement = element; OriginalName = element.GetAttributeString("name", ""); name = OriginalName; VariantOf = element.VariantOf(); if (!VariantOf.IsEmpty) { return; } //don't even attempt to read the XML until the PrefabCollection readies up the parent to inherit from ParseConfigElement(variantOf: null); } private string GetTexturePath(ContentXElement subElement, ItemPrefab variantOf) => subElement.DoesAttributeReferenceFileNameAlone("texture") ? Path.GetDirectoryName(variantOf?.ContentFile.Path ?? ContentFile.Path) : ""; private void ParseConfigElement(ItemPrefab variantOf) { string categoryStr = ConfigElement.GetAttributeString("category", "Misc"); this.category = Enum.TryParse(categoryStr, true, out MapEntityCategory category) ? category : MapEntityCategory.Misc; //nameidentifier can be used to make multiple items use the same names and descriptions Identifier nameIdentifier = ConfigElement.GetAttributeIdentifier("nameidentifier", ""); //only used if the item doesn't have a name/description defined in the currently selected language string fallbackNameIdentifier = ConfigElement.GetAttributeString("fallbacknameidentifier", ""); name = TextManager.Get(nameIdentifier.IsEmpty ? $"EntityName.{Identifier}" : $"EntityName.{nameIdentifier}", $"EntityName.{fallbackNameIdentifier}"); if (!string.IsNullOrEmpty(OriginalName)) { name = name.Fallback(OriginalName); } if (category == MapEntityCategory.Wrecked) { name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); } name = GeneticMaterial.TryCreateName(this, ConfigElement); this.aliases = (ConfigElement.GetAttributeStringArray("aliases", null, convertToLowerInvariant: true) ?? ConfigElement.GetAttributeStringArray("Aliases", Array.Empty(), convertToLowerInvariant: true)) .ToImmutableHashSet() .Add(OriginalName.ToLowerInvariant()); var triggers = new List(); var deconstructItems = new List(); var fabricationRecipes = new Dictionary(); var treatmentSuitability = new Dictionary(); var storePrices = new Dictionary(); var preferredContainers = new List(); DeconstructTime = 1.0f; if (ConfigElement.GetAttribute("allowasextracargo") != null) { AllowAsExtraCargo = ConfigElement.GetAttributeBool("allowasextracargo", false); } this.tags = ConfigElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet(); if (!Tags.Any()) { this.tags = ConfigElement.GetAttributeIdentifierArray("Tags", Array.Empty()).ToImmutableHashSet(); } if (ConfigElement.GetAttribute("cargocontainername") != null) { DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name."); } SerializableProperty.DeserializeProperties(this, ConfigElement); LoadDescription(ConfigElement); var allowDroppingOnSwapWith = ConfigElement.GetAttributeIdentifierArray("allowdroppingonswapwith", Array.Empty()); AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet(); AllowDroppingOnSwap = allowDroppingOnSwapWith.Any(); var levelCommonness = new Dictionary(); var levelQuantity = new Dictionary(); foreach (ContentXElement subElement in ConfigElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": string spriteFolder = GetTexturePath(subElement, variantOf); canSpriteFlipX = subElement.GetAttributeBool("canflipx", true); canSpriteFlipY = subElement.GetAttributeBool("canflipy", true); sprite = new Sprite(subElement, spriteFolder, lazyLoad: true); if (subElement.GetAttribute("sourcerect") == null && subElement.GetAttribute("sheetindex") == null) { DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!"); } Size = Sprite.size; if (subElement.GetAttribute("name") == null && !Name.IsNullOrWhiteSpace()) { Sprite.Name = Name.Value; } Sprite.EntityIdentifier = Identifier; break; case "price": if (subElement.GetAttribute("baseprice") != null) { foreach (var priceInfo in PriceInfo.CreatePriceInfos(subElement, out defaultPrice)) { if (priceInfo.StoreIdentifier.IsEmpty) { continue; } if (storePrices.ContainsKey(priceInfo.StoreIdentifier)) { DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the store \"{priceInfo.StoreIdentifier}\" defined more than once."); storePrices[priceInfo.StoreIdentifier] = priceInfo; } else { storePrices.Add(priceInfo.StoreIdentifier, priceInfo); } } } else if (subElement.GetAttribute("buyprice") != null && subElement.GetAttributeIdentifier("locationtype", "") is { IsEmpty: false } locationType) // Backwards compatibility { if (storePrices.ContainsKey(locationType)) { DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the location type \"{locationType}\" defined more than once."); storePrices[locationType] = new PriceInfo(subElement); } else { storePrices.Add(locationType, new PriceInfo(subElement)); } } break; case "deconstruct": DeconstructTime = subElement.GetAttributeFloat("time", 1.0f); AllowDeconstruct = true; RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false); RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1); foreach (XElement itemElement in subElement.Elements()) { if (itemElement.Attribute("name") != null) { DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items."); continue; } var deconstructItem = new DeconstructItem(itemElement, Identifier); if (deconstructItem.ItemIdentifier.IsEmpty) { DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier."); continue; } deconstructItems.Add(deconstructItem); } RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, deconstructItems.Count); break; case "fabricate": case "fabricable": case "fabricableitem": var newRecipe = new FabricationRecipe(subElement, Identifier); if (fabricationRecipes.TryGetValue(newRecipe.RecipeHash, out var prevRecipe)) { DebugConsole.ThrowError( $"Error in item prefab \"{ToString()}\": " + $"{prevRecipe.DisplayName} has the same hash as {newRecipe.DisplayName}. " + $"This will cause issues with fabrication." ); } else { fabricationRecipes.Add(newRecipe.RecipeHash, newRecipe); } break; case "preferredcontainer": var preferredContainer = new PreferredContainer(subElement); if (preferredContainer.Primary.Count == 0 && preferredContainer.Secondary.Count == 0) { DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement})."); } else { preferredContainers.Add(preferredContainer); } break; case "swappableitem": SwappableItem = new SwappableItem(subElement); break; case "trigger": Rectangle trigger = new Rectangle(0, 0, 10, 10) { X = subElement.GetAttributeInt("x", 0), Y = subElement.GetAttributeInt("y", 0), Width = subElement.GetAttributeInt("width", 0), Height = subElement.GetAttributeInt("height", 0) }; triggers.Add(trigger); break; case "levelresource": foreach (XElement levelCommonnessElement in subElement.GetChildElements("commonness")) { Identifier levelName = levelCommonnessElement.GetAttributeIdentifier("leveltype", ""); if (!levelCommonnessElement.GetAttributeBool("fixedquantity", false)) { if (!levelCommonness.ContainsKey(levelName)) { levelCommonness.Add(levelName, new CommonnessInfo(levelCommonnessElement)); } } else { if (!levelQuantity.ContainsKey(levelName)) { levelQuantity.Add(levelName, new FixedQuantityResourceInfo( levelCommonnessElement.GetAttributeInt("clusterquantity", 0), levelCommonnessElement.GetAttributeInt("clustersize", 0), levelCommonnessElement.GetAttributeBool("isislandspecific", false), levelCommonnessElement.GetAttributeBool("allowatstart", true))); } } } break; case "suitabletreatment": if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names."); } Identifier treatmentIdentifier = subElement.GetAttributeIdentifier("identifier", subElement.GetAttributeIdentifier("type", Identifier.Empty)); float suitability = subElement.GetAttributeFloat("suitability", 0.0f); treatmentSuitability.Add(treatmentIdentifier, suitability); break; } } #if CLIENT ParseSubElementsClient(ConfigElement, variantOf); #endif this.Triggers = triggers.ToImmutableArray(); this.DeconstructItems = deconstructItems.ToImmutableArray(); this.FabricationRecipes = fabricationRecipes.ToImmutableDictionary(); this.treatmentSuitability = treatmentSuitability.ToImmutableDictionary(); StorePrices = storePrices.ToImmutableDictionary(); this.PreferredContainers = preferredContainers.ToImmutableArray(); this.LevelCommonness = levelCommonness.ToImmutableDictionary(); this.LevelQuantity = levelQuantity.ToImmutableDictionary(); // Backwards compatibility if (storePrices.Any()) { defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); } HideConditionInTooltip = ConfigElement.GetAttributeBool("hideconditionintooltip", HideConditionBar); //backwards compatibility if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase)) { this.category = MapEntityCategory.Wrecked; Subcategory = "Thalamus"; } if (Sprite == null) { DebugConsole.ThrowError($"Item \"{ToString()}\" has no sprite!"); #if SERVER this.sprite = new Sprite("", Vector2.Zero); this.sprite.SourceRect = new Rectangle(0, 0, 32, 32); #else this.sprite = new Sprite(TextureLoader.PlaceHolderTexture, null, null) { Origin = TextureLoader.PlaceHolderTexture.Bounds.Size.ToVector2() / 2 }; #endif Size = Sprite.size; Sprite.EntityIdentifier = Identifier; } if (Identifier == Identifier.Empty) { DebugConsole.ThrowError( $"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."); } #if DEBUG if (!Category.HasFlag(MapEntityCategory.Legacy) && !HideInMenus) { if (!string.IsNullOrEmpty(OriginalName)) { DebugConsole.AddWarning($"Item \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages."); } } #endif this.allowedLinks = ConfigElement.GetAttributeIdentifierArray("allowedlinks", Array.Empty()).ToImmutableHashSet(); } public CommonnessInfo? GetCommonnessInfo(Level level) { CommonnessInfo? levelCommonnessInfo = GetValueOrNull(level.GenerationParams.Identifier); CommonnessInfo? biomeCommonnessInfo = GetValueOrNull(level.LevelData.Biome.Identifier); CommonnessInfo? defaultCommonnessInfo = GetValueOrNull(Identifier.Empty); if (levelCommonnessInfo.HasValue) { return levelCommonnessInfo?.WithInheritedCommonness(biomeCommonnessInfo, defaultCommonnessInfo); } else if (biomeCommonnessInfo.HasValue) { return biomeCommonnessInfo?.WithInheritedCommonness(defaultCommonnessInfo); } else if (defaultCommonnessInfo.HasValue) { return defaultCommonnessInfo; } return null; CommonnessInfo? GetValueOrNull(Identifier identifier) { if (LevelCommonness.TryGetValue(identifier, out CommonnessInfo info)) { return info; } else { return null; } } } public float GetTreatmentSuitability(Identifier treatmentIdentifier) { return treatmentSuitability.TryGetValue(treatmentIdentifier, out float suitability) ? suitability : 0.0f; } #region Pricing public PriceInfo GetPriceInfo(Location.StoreInfo store) { if (store == null) { string message = $"Tried to get price info for \"{Identifier}\" with a null store parameter!\n{Environment.StackTrace.CleanupStackTrace()}"; #if DEBUG DebugConsole.LogError(message); #else DebugConsole.AddWarning(message); GameAnalyticsManager.AddErrorEventOnce("ItemPrefab.GetPriceInfo:StoreParameterNull", GameAnalyticsManager.ErrorSeverity.Error, message); #endif return null; } else if (!store.Identifier.IsEmpty && StorePrices != null && StorePrices.TryGetValue(store.Identifier, out var storePriceInfo)) { return storePriceInfo; } else { return DefaultPrice; } } public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo) { priceInfo = GetPriceInfo(store); return priceInfo is { CanBeBought: true } && (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; } public bool CanBeBoughtFrom(Location location) { if (location?.Stores == null) { return false; } foreach (var store in location.Stores) { var priceInfo = GetPriceInfo(store.Value); if (priceInfo == null) { continue; } if (!priceInfo.CanBeBought) { continue; } if (location.LevelData.Difficulty < priceInfo.MinLevelDifficulty) { continue; } return true; } return false; } public int? GetMinPrice() { int? minPrice = null; if (StorePrices != null && StorePrices.Any()) { minPrice = StorePrices.Values.Min(p => p.Price); } if (minPrice.HasValue) { if (DefaultPrice != null) { return minPrice < DefaultPrice.Price ? minPrice : DefaultPrice.Price; } else { return minPrice.Value; } } else { return DefaultPrice?.Price; } } public ImmutableDictionary GetBuyPricesUnder(int maxCost = 0) { var prices = new Dictionary(); if (StorePrices != null) { foreach (var storePrice in StorePrices) { var priceInfo = storePrice.Value; if (priceInfo == null) { continue; } if (!priceInfo.CanBeBought) { continue; } if (priceInfo.Price < maxCost || maxCost == 0) { prices.Add(storePrice.Key, priceInfo); } } } return prices.ToImmutableDictionary(); } public ImmutableDictionary GetSellPricesOver(int minCost = 0, bool sellingImportant = true) { var prices = new Dictionary(); if (!CanBeSold && sellingImportant) { return prices.ToImmutableDictionary(); } foreach (var storePrice in StorePrices) { var priceInfo = storePrice.Value; if (priceInfo == null) { continue; } if (priceInfo.Price > minCost) { prices.Add(storePrice.Key, priceInfo); } } return prices.ToImmutableDictionary(); } #endregion public static ItemPrefab Find(string name, Identifier identifier) { if (string.IsNullOrEmpty(name) && identifier.IsEmpty) { throw new ArgumentException("Both name and identifier cannot be null."); } if (identifier.IsEmpty) { //legacy support identifier = GenerateLegacyIdentifier(name); } Prefabs.TryGet(identifier, out ItemPrefab prefab); //not found, see if we can find a prefab with a matching alias if (prefab == null && !string.IsNullOrEmpty(name)) { string lowerCaseName = name.ToLowerInvariant(); prefab = Prefabs.Find(me => me.Aliases != null && me.Aliases.Contains(lowerCaseName)); } if (prefab == null) { prefab = Prefabs.Find(me => me.Aliases != null && me.Aliases.Contains(identifier.Value)); } if (prefab == null) { DebugConsole.ThrowError($"Error loading item - item prefab \"{name}\" (identifier \"{identifier}\") not found."); } return prefab; } public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false, bool checkTransferConditions = false) { isPreferencesDefined = PreferredContainers.Any(); isSecondary = false; if (!isPreferencesDefined) { return true; } if (PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, targetContainer) && (!checkTransferConditions || CanBeTransferred(item.Prefab.Identifier, pc, targetContainer)))) { return true; } isSecondary = true; return PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, targetContainer)); static bool HasConditionRequirement(PreferredContainer pc) => pc.MinCondition > 0 || pc.MaxCondition < 100; } public bool IsContainerPreferred(Item item, Identifier[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary) { isPreferencesDefined = PreferredContainers.Any(); isSecondary = false; if (!isPreferencesDefined) { return true; } if (PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, identifiersOrTags))) { return true; } isSecondary = true; return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, identifiersOrTags)); } private static bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; private static bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) => pc.AllowTransfersHere && (!pc.TransferOnlyOnePerContainer || targetContainer.Inventory.AllItems.None(i => i.Prefab.Identifier == item)); public static bool IsContainerPreferred(IEnumerable preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id)); public static bool IsContainerPreferred(IEnumerable preferences, IEnumerable ids) => ids.Any(id => preferences.Contains(id)); protected override void CreateInstance(Rectangle rect) { throw new InvalidOperationException("Can't call ItemPrefab.CreateInstance"); } public override void Dispose() { Item.RemoveByPrefab(this); } public Identifier VariantOf { get; } public void InheritFrom(ItemPrefab parent) { ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); ParseConfigElement(parent); } public override string ToString() { return $"{Name} (identifier: {Identifier})"; } } }