using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; namespace Barotrauma { struct DeconstructItem { public readonly string ItemIdentifier; //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 float Commonness { get; } public DeconstructItem(XElement element, string parentDebugName) { ItemIdentifier = element.GetAttributeString("identifier", "notfound"); 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); if (element.Attribute("copycondition") != null && element.Attribute("outcondition") != null) { DebugConsole.AddWarning($"Invalid deconstruction output in \"{parentDebugName}\": the output item \"{ItemIdentifier}\" has the out condition set, but is also set to copy the condition of the deconstructed item. Ignoring the out condition."); } RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", new string[0]); RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]); ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty); InfoText = element.GetAttributeString("infotext", string.Empty); InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty); } } class FabricationRecipe { public class RequiredItem { public readonly List ItemPrefabs; public int Amount; public readonly float MinCondition; public readonly float MaxCondition; public readonly bool UseCondition; public RequiredItem(ItemPrefab itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition) { ItemPrefabs = new List() { itemPrefab }; Amount = amount; MinCondition = minCondition; MaxCondition = maxCondition; UseCondition = useCondition; } public RequiredItem(IEnumerable itemPrefabs, int amount, float minCondition, float maxCondition, bool useCondition) { ItemPrefabs = new List(itemPrefabs); Amount = amount; MinCondition = minCondition; MaxCondition = maxCondition; UseCondition = useCondition; } } public readonly ItemPrefab TargetItem; public readonly string DisplayName; public readonly List RequiredItems; public readonly string[] SuitableFabricatorIdentifiers; public readonly float RequiredTime; public readonly bool RequiresRecipe; public readonly float OutCondition; //Percentage-based from 0 to 1 public readonly List RequiredSkills; public int Amount { get; } public FabricationRecipe(XElement element, ItemPrefab itemPrefab) { TargetItem = itemPrefab; string displayName = element.GetAttributeString("displayname", ""); DisplayName = string.IsNullOrEmpty(displayName) ? itemPrefab.Name : TextManager.GetWithVariable($"DisplayName.{displayName}", "[itemname]", itemPrefab.Name); SuitableFabricatorIdentifiers = element.GetAttributeStringArray("suitablefabricators", new string[0]); RequiredSkills = new List(); RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); RequiredItems = new List(); RequiresRecipe = element.GetAttributeBool("requiresrecipe", false); Amount = element.GetAttributeInt("amount", 1); foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "requiredskill": if (subElement.Attribute("name") != null) { DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Use skill identifiers instead of names."); continue; } RequiredSkills.Add(new Skill( subElement.GetAttributeString("identifier", ""), subElement.GetAttributeInt("level", 0))); break; case "item": case "requireditem": string requiredItemIdentifier = subElement.GetAttributeString("identifier", ""); string requiredItemTag = subElement.GetAttributeString("tag", ""); if (string.IsNullOrWhiteSpace(requiredItemIdentifier) && string.IsNullOrEmpty(requiredItemTag)) { DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! 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 count = subElement.GetAttributeInt("count", 1); if (!string.IsNullOrEmpty(requiredItemIdentifier)) { if (!(MapEntityPrefab.Find(null, requiredItemIdentifier.Trim()) is ItemPrefab requiredItem)) { DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Required item \"" + requiredItemIdentifier + "\" not found."); continue; } var existing = RequiredItems.Find(r => r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem && MathUtils.NearlyEqual(r.MinCondition, minCondition) && MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); if (existing == null) { RequiredItems.Add(new RequiredItem(requiredItem, count, minCondition, maxCondition, useCondition)); } else { existing.Amount += count; } } else { var matchingItems = ItemPrefab.Prefabs.Where(ip => ip.Tags.Any(t => t.Equals(requiredItemTag, StringComparison.OrdinalIgnoreCase))); if (!matchingItems.Any()) { DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Could not find any items with the tag \"" + requiredItemTag + "\"."); continue; } var existing = RequiredItems.Find(r => r.ItemPrefabs.SequenceEqual(matchingItems) && MathUtils.NearlyEqual(r.MinCondition, minCondition) && MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); if (existing == null) { RequiredItems.Add(new RequiredItem(matchingItems, count, minCondition, maxCondition, useCondition)); } else { existing.Amount += count; } } break; } } } } class PreferredContainer { public readonly HashSet Primary = new HashSet(); public readonly HashSet Secondary = new HashSet(); public float SpawnProbability { get; private set; } public float MaxCondition { get; private set; } public float MinCondition { get; private set; } public int MinAmount { get; private set; } public int MaxAmount { get; private set; } public PreferredContainer(XElement element) { Primary = XMLExtensions.GetAttributeStringArray(element, "primary", new string[0]).ToHashSet(); Secondary = XMLExtensions.GetAttributeStringArray(element, "secondary", new string[0]).ToHashSet(); SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f); MinAmount = element.GetAttributeInt("minamount", 0); MaxAmount = Math.Max(MinAmount, element.GetAttributeInt("maxamount", 0)); MaxCondition = element.GetAttributeFloat("maxcondition", 100f); MinCondition = element.GetAttributeFloat("mincondition", 0f); if (element.Attribute("spawnprobability") == null) { //if spawn probability is not defined but amount is, assume the probability is 1 if (MaxAmount > 0) { SpawnProbability = 1.0f; } } else if (element.Attribute("minamount") == null && element.Attribute("maxamount") == null) { //spawn probability defined but amount isn't, assume amount is 1 MinAmount = MaxAmount = 1; SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f); } } } class SwappableItem { public int BasePrice { get; } public readonly bool CanBeBought; public readonly string ReplacementOnUninstall; public string SpawnWithId; public string SwapIdentifier; public readonly Vector2 SwapOrigin; public List<(string requiredTag, string swapTo)> ConnectedItemsToSwap = new List<(string requiredTag, string swapTo)>(); public readonly Sprite SchematicSprite; public int GetPrice(Location location = null) { int price = BasePrice; return location?.GetAdjustedMechanicalCost(price) ?? price; } public SwappableItem(XElement element) { BasePrice = Math.Max(element.GetAttributeInt("price", 0), 0); SwapIdentifier = element.GetAttributeString("swapidentifier", string.Empty); CanBeBought = element.GetAttributeBool("canbebought", BasePrice != 0); ReplacementOnUninstall = element.GetAttributeString("replacementonuninstall", ""); SwapOrigin = element.GetAttributeVector2("origin", Vector2.One); SpawnWithId = element.GetAttributeString("spawnwithid", string.Empty); foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "schematicsprite": SchematicSprite = new Sprite(subElement); break; case "swapconnecteditem": ConnectedItemsToSwap.Add( (subElement.GetAttributeString("tag", ""), subElement.GetAttributeString("swapto", ""))); break; } } } } partial class ItemPrefab : MapEntityPrefab, IHasUintIdentifier { private readonly string name; public override string Name => name; public static readonly PrefabCollection Prefabs = new PrefabCollection(); private bool disposed = false; public override void Dispose() { if (disposed) { return; } disposed = true; Prefabs.Remove(this); Item.RemoveByPrefab(this); } //default size protected Vector2 size; private float impactTolerance; public readonly PriceInfo DefaultPrice; private readonly Dictionary locationPrices; /// /// 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 List Triggers; private readonly List fabricationRecipeElements = new List(); private readonly Dictionary treatmentSuitability = new Dictionary(); /// /// Is this prefab overriding a prefab in another content package /// public bool IsOverride; public readonly ItemPrefab VariantOf; public XElement ConfigElement { get; private set; } public List DeconstructItems { get; private set; } public List FabricationRecipes { get; private set; } public float DeconstructTime { get; private set; } public bool AllowDeconstruct { get; private set; } //how close the Character has to be to the item to pick it up [Serialize(120.0f, false)] public float InteractDistance { get; private set; } // this can be used to allow items which are behind other items tp [Serialize(0.0f, false)] public float InteractPriority { get; private set; } [Serialize(false, false)] public bool InteractThroughWalls { get; private set; } [Serialize(false, false, description: "Hides the condition bar displayed at the bottom of the inventory slot the item is in.")] public bool HideConditionBar { get; set; } [Serialize(false, false, 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, false)] 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, false)] 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, false)] public bool RequireCampaignInteract { get; private set; } //should the camera focus on the item when selected [Serialize(false, false)] public bool FocusOnSelected { get; private set; } //the amount of "camera offset" when selecting the construction [Serialize(0.0f, false)] public float OffsetOnSelected { get; private set; } [Serialize(100.0f, false)] public float Health { get; private set; } [Serialize(false, false)] public bool AllowSellingWhenBroken { get; private set; } [Serialize(false, false)] public bool Indestructible { get; private set; } [Serialize(false, false)] public bool DamagedByExplosions { get; private set; } [Serialize(1f, false)] public float ExplosionDamageMultiplier { get; private set; } [Serialize(false, false)] public bool DamagedByProjectiles { get; private set; } [Serialize(false, false)] public bool DamagedByMeleeWeapons { get; private set; } [Serialize(false, false)] public bool DamagedByRepairTools { get; private set; } [Serialize(false, false)] public bool DamagedByMonsters { get; private set; } [Serialize(false, false)] public bool FireProof { get; private set; } [Serialize(false, false)] public bool WaterProof { get; private set; } [Serialize(0.0f, false)] public float ImpactTolerance { get { return impactTolerance; } set { impactTolerance = Math.Max(value, 0.0f); } } [Serialize(0.0f, false)] public float SonarSize { get; private set; } [Serialize(false, false)] public bool UseInHealthInterface { get; private set; } [Serialize(false, false)] public bool DisableItemUsageWhenSelected { get; private set; } [Serialize("", false)] public string CargoContainerIdentifier { get; private set; } [Serialize(false, false)] public bool UseContainedSpriteColor { get; private set; } [Serialize(false, false)] public bool UseContainedInventoryIconColor { get; private set; } [Serialize(0.0f, false)] public float AddedRepairSpeedMultiplier { get; private set; } [Serialize(false, false)] public bool CannotRepairFail { get; private set; } [Serialize(null, false)] public string EquipConfirmationText { get; set; } [Serialize(true, false, description: "Can the item be rotated in the submarine editor.")] public bool AllowRotatingInEditor { get; set; } [Serialize(false, false)] public bool ShowContentsInTooltip { get; private set; } //Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced. public List PreferredContainers { get; private set; } = new List(); public SwappableItem SwappableItem { get; private set; } /// /// How likely it is for the item to spawn in a level of a given type. /// Key = name of the LevelGenerationParameters (empty string = default value) /// Value = commonness /// public Dictionary LevelCommonness { get; private set; } = new Dictionary(); public Dictionary LevelQuantity { get; } = new Dictionary(); public struct FixedQuantityResourceInfo { public int ClusterQuantity { get; } public int ClusterSize { get; } public bool IsIslandSpecifc { get; } public bool AllowAtStart { get; } public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific, bool allowAtStart) { ClusterQuantity = clusterQuantity; ClusterSize = clusterSize; IsIslandSpecifc = isIslandSpecific; AllowAtStart = allowAtStart; } } [Serialize(true, false)] public bool CanFlipX { get; private set; } [Serialize(true, false)] public bool CanFlipY { get; private set; } [Serialize(false, false)] public bool IsDangerous { get; private set; } public bool CanSpriteFlipX { get; private set; } public bool CanSpriteFlipY { get; private set; } private int maxStackSize; [Serialize(1, false)] public int MaxStackSize { get { return maxStackSize; } set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); } } [Serialize(false, false)] public bool AllowDroppingOnSwap { get; private set; } private readonly HashSet allowDroppingOnSwapWith = new HashSet(); public IEnumerable AllowDroppingOnSwapWith { get { return allowDroppingOnSwapWith; } } public Vector2 Size => size; public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought)); /// /// 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; /// /// Any item with a Price element in the definition can be sold everywhere. /// public bool CanBeSold => DefaultPrice != null; public bool RandomDeconstructionOutput { get; } public int RandomDeconstructionOutputAmount { get; } public uint UIntIdentifier { get; set; } public static void RemoveByFile(string filePath) => Prefabs.RemoveByFile(filePath); public static void LoadFromFile(ContentFile file) { DebugConsole.Log("*** " + file.Path + " ***"); RemoveByFile(file.Path); XDocument doc = XMLExtensions.TryLoadXml(file.Path); if (doc == null) { return; } var rootElement = doc.Root; switch (rootElement.Name.ToString().ToLowerInvariant()) { case "item": new ItemPrefab(rootElement, file.Path, false) { ContentPackage = file.ContentPackage }; break; case "items": foreach (var element in rootElement.Elements()) { if (element.IsOverride()) { var itemElement = element.GetChildElement("item"); if (itemElement != null) { new ItemPrefab(itemElement, file.Path, true) { ContentPackage = file.ContentPackage, IsOverride = true }; } else { DebugConsole.ThrowError($"Cannot find an item element from the children of the override element defined in {file.Path}"); } } else { new ItemPrefab(element, file.Path, false) { ContentPackage = file.ContentPackage }; } } break; case "override": var items = rootElement.GetChildElement("items"); if (items != null) { foreach (var element in items.Elements()) { new ItemPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage, IsOverride = true }; } } foreach (var element in rootElement.GetChildElements("item")) { new ItemPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; } break; default: DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}"); break; } } public static void LoadAll(IEnumerable files) { DebugConsole.Log("Loading item prefabs: "); foreach (ContentFile file in files) { LoadFromFile(file); } //initialize item requirements for fabrication recipes //(has to be done after all the item prefabs have been loaded, because the //recipe ingredients may not have been loaded yet when loading the prefab) InitFabricationRecipes(); } public static void InitFabricationRecipes() { foreach (ItemPrefab itemPrefab in Prefabs) { itemPrefab.FabricationRecipes.Clear(); foreach (XElement fabricationRecipe in itemPrefab.fabricationRecipeElements) { itemPrefab.FabricationRecipes.Add(new FabricationRecipe(fabricationRecipe, itemPrefab)); } } } public static string GenerateLegacyIdentifier(string name) { return "legacyitem_" + name.ToLowerInvariant().Replace(" ", ""); } public ItemPrefab(XElement element, string filePath, bool allowOverriding) { FilePath = filePath; ConfigElement = element; originalName = element.GetAttributeString("name", ""); name = originalName; identifier = element.GetAttributeString("identifier", ""); string variantOf = element.GetAttributeString("variantof", ""); if (!string.IsNullOrEmpty(variantOf)) { ItemPrefab basePrefab = Find(null, variantOf); if (basePrefab == null) { DebugConsole.ThrowError($"Failed to load the item variant \"{identifier}\" - could not find the base prefab \"{variantOf}\""); } else { VariantOf = basePrefab; ConfigElement = element = CreateVariantXML(element, basePrefab); } } string categoryStr = element.GetAttributeString("category", "Misc"); if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) { category = MapEntityCategory.Misc; } Category = category; var parentType = element.Parent?.GetAttributeString("itemtype", "") ?? string.Empty; //nameidentifier can be used to make multiple items use the same names and descriptions string nameIdentifier = element.GetAttributeString("nameidentifier", ""); //only used if the item doesn't have a name/description defined in the currently selected language string fallbackNameIdentifier = element.GetAttributeString("fallbacknameidentifier", ""); //works the same as nameIdentifier, but just replaces the description string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); if (string.IsNullOrEmpty(originalName)) { if (string.IsNullOrEmpty(nameIdentifier)) { name = TextManager.Get("EntityName." + identifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; } else { name = TextManager.Get("EntityName." + nameIdentifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; } } else if (Category.HasFlag(MapEntityCategory.Legacy)) { // Legacy items use names as identifiers, so we have to define them in the xml. But we also want to support the translations. Therefore if (string.IsNullOrEmpty(nameIdentifier)) { name = TextManager.Get("EntityName." + identifier, true) ?? originalName; } else { name = TextManager.Get("EntityName." + nameIdentifier, true) ?? originalName; } if (string.IsNullOrWhiteSpace(identifier)) { identifier = GenerateLegacyIdentifier(originalName); } } if (string.Equals(parentType, "wrecked", StringComparison.OrdinalIgnoreCase)) { if (!string.IsNullOrEmpty(name)) { name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); } } name = GeneticMaterial.TryCreateName(this, element); if (string.IsNullOrEmpty(name)) { DebugConsole.ThrowError($"Unnamed item ({identifier}) in {filePath}!"); } DebugConsole.Log(" " + name); Aliases = new HashSet (element.GetAttributeStringArray("aliases", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("Aliases", new string[0], convertToLowerInvariant: true)); Aliases.Add(originalName.ToLowerInvariant()); Triggers = new List(); DeconstructItems = new List(); FabricationRecipes = new List(); DeconstructTime = 1.0f; if (element.Attribute("allowasextracargo") != null) { AllowAsExtraCargo = element.GetAttributeBool("allowasextracargo", false); } Tags = new HashSet(element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true)); if (!Tags.Any()) { Tags = new HashSet(element.GetAttributeStringArray("Tags", new string[0], convertToLowerInvariant: true)); } if (element.Attribute("cargocontainername") != null) { DebugConsole.ThrowError("Error in item prefab \"" + name + "\" - cargo container should be configured using the item's identifier, not the name."); } SerializableProperty.DeserializeProperties(this, element); if (string.IsNullOrEmpty(Description)) { if (!string.IsNullOrEmpty(descriptionIdentifier)) { Description = TextManager.Get("EntityDescription." + descriptionIdentifier, true) ?? string.Empty; } else if (string.IsNullOrEmpty(nameIdentifier)) { Description = TextManager.Get("EntityDescription." + identifier, true) ?? string.Empty; } else { Description = TextManager.Get("EntityDescription." + nameIdentifier, true) ?? string.Empty; } } var allowDroppingOnSwapWith = element.GetAttributeStringArray("allowdroppingonswapwith", new string[0]); if (allowDroppingOnSwapWith.Any()) { AllowDroppingOnSwap = true; foreach (string tag in allowDroppingOnSwapWith) { this.allowDroppingOnSwapWith.Add(tag); } } foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": string spriteFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { spriteFolder = Path.GetDirectoryName(filePath); } CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true); CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); sprite = new Sprite(subElement, spriteFolder, lazyLoad: true); if (subElement.Attribute("sourcerect") == null && subElement.Attribute("sheetindex") == null) { DebugConsole.ThrowError("Warning - sprite sourcerect not configured for item \"" + Name + "\"!"); } size = sprite.size; if (subElement.Attribute("name") == null && !string.IsNullOrWhiteSpace(Name)) { sprite.Name = Name; } sprite.EntityID = identifier; break; case "price": if (locationPrices == null) { locationPrices = new Dictionary(); } if (subElement.Attribute("baseprice") != null) { foreach (Tuple priceInfo in PriceInfo.CreatePriceInfos(subElement, out DefaultPrice)) { if (priceInfo == null) { continue; } locationPrices.Add(priceInfo.Item1, priceInfo.Item2); } } else if (subElement.Attribute("buyprice") != null) { string locationType = subElement.GetAttributeString("locationtype", "").ToLowerInvariant(); locationPrices.Add(locationType, new PriceInfo(subElement)); } break; case "upgradeoverride": { #if CLIENT var sprites = new List(); foreach (XElement decorSprite in subElement.Elements()) { if (decorSprite.Name.ToString().Equals("decorativesprite", StringComparison.OrdinalIgnoreCase)) { sprites.Add(new DecorativeSprite(decorSprite)); } } UpgradeOverrideSprites.Add(subElement.GetAttributeString("identifier", ""), sprites); #endif break; } #if CLIENT case "upgradepreviewsprite": { string iconFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { iconFolder = Path.GetDirectoryName(filePath); } UpgradePreviewSprite = new Sprite(subElement, iconFolder, lazyLoad: true); UpgradePreviewScale = subElement.GetAttributeFloat("scale", 1.0f); } break; case "inventoryicon": { string iconFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { iconFolder = Path.GetDirectoryName(filePath); } InventoryIcon = new Sprite(subElement, iconFolder, lazyLoad: true); } break; case "minimapicon": { string iconFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { iconFolder = Path.GetDirectoryName(filePath); } MinimapIcon = new Sprite(subElement, iconFolder, lazyLoad: true); } break; case "infectedsprite": { string iconFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { iconFolder = Path.GetDirectoryName(filePath); } InfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); } break; case "damagedinfectedsprite": { string iconFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { iconFolder = Path.GetDirectoryName(filePath); } DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); } break; case "brokensprite": string brokenSpriteFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { brokenSpriteFolder = Path.GetDirectoryName(filePath); } var brokenSprite = new BrokenItemSprite( new Sprite(subElement, brokenSpriteFolder, lazyLoad: true), subElement.GetAttributeFloat("maxcondition", 0.0f), subElement.GetAttributeBool("fadein", false), subElement.GetAttributePoint("offset", Point.Zero)); int spriteIndex = 0; for (int i = 0; i < BrokenSprites.Count && BrokenSprites[i].MaxCondition < brokenSprite.MaxCondition; i++) { spriteIndex = i; } BrokenSprites.Insert(spriteIndex, brokenSprite); break; case "decorativesprite": string decorativeSpriteFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { decorativeSpriteFolder = Path.GetDirectoryName(filePath); } int groupID = 0; DecorativeSprite decorativeSprite = null; if (subElement.Attribute("texture") == null) { groupID = subElement.GetAttributeInt("randomgroupid", 0); } else { decorativeSprite = new DecorativeSprite(subElement, decorativeSpriteFolder, lazyLoad: true); DecorativeSprites.Add(decorativeSprite); groupID = decorativeSprite.RandomGroupID; } if (!DecorativeSpriteGroups.ContainsKey(groupID)) { DecorativeSpriteGroups.Add(groupID, new List()); } DecorativeSpriteGroups[groupID].Add(decorativeSprite); break; case "containedsprite": string containedSpriteFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { containedSpriteFolder = Path.GetDirectoryName(filePath); } var containedSprite = new ContainedItemSprite(subElement, containedSpriteFolder, lazyLoad: true); if (containedSprite.Sprite != null) { ContainedSprites.Add(containedSprite); } break; #endif case "deconstruct": DeconstructTime = subElement.GetAttributeFloat("time", 1.0f); AllowDeconstruct = true; RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false); RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1); foreach (XElement deconstructItem in subElement.Elements()) { if (deconstructItem.Attribute("name") != null) { DebugConsole.ThrowError("Error in item config \"" + Name + "\" - use item identifiers instead of names to configure the deconstruct items."); continue; } DeconstructItems.Add(new DeconstructItem(deconstructItem, identifier)); } RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, DeconstructItems.Count); break; case "fabricate": case "fabricable": case "fabricableitem": fabricationRecipeElements.Add(subElement); break; case "preferredcontainer": var preferredContainer = new PreferredContainer(subElement); if (preferredContainer.Primary.Count == 0 && preferredContainer.Secondary.Count == 0) { DebugConsole.ThrowError($"Error in item prefab {Name}: 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")) { string levelName = levelCommonnessElement.GetAttributeString("leveltype", "").ToLowerInvariant(); if (!levelCommonnessElement.GetAttributeBool("fixedquantity", false)) { if (!LevelCommonness.ContainsKey(levelName)) { LevelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f)); } } 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.Attribute("name") != null) { DebugConsole.ThrowError("Error in item prefab \"" + Name + "\" - suitable treatments should be defined using item identifiers, not item names."); } string treatmentIdentifier = (subElement.GetAttributeString("identifier", null) ?? subElement.GetAttributeString("type", string.Empty)).ToLowerInvariant(); float suitability = subElement.GetAttributeFloat("suitability", 0.0f); treatmentSuitability.Add(treatmentIdentifier, suitability); break; } } // Set the default price in case the prices are defined in the old way // with separate Price elements and there is no default price explicitly set if (locationPrices != null && locationPrices.Any()) { DefaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); } HideConditionInTooltip = element.GetAttributeBool("hideconditionintooltip", HideConditionBar); //backwards compatibility if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase)) { Category = MapEntityCategory.Wrecked; Subcategory = "Thalamus"; } if (sprite == null) { DebugConsole.ThrowError("Item \"" + Name + "\" has no sprite!"); #if SERVER sprite = new Sprite("", Vector2.Zero); sprite.SourceRect = new Rectangle(0, 0, 32, 32); #else sprite = new Sprite(TextureLoader.PlaceHolderTexture, null, null) { Origin = TextureLoader.PlaceHolderTexture.Bounds.Size.ToVector2() / 2 }; #endif size = sprite.size; sprite.EntityID = identifier; } if (string.IsNullOrEmpty(identifier)) { DebugConsole.ThrowError( "Item prefab \"" + name + "\" 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 \"{(string.IsNullOrEmpty(identifier) ? name : identifier)}\" has a hard-coded name, and won't be localized to other languages."); } } #endif AllowedLinks = element.GetAttributeStringArray("allowedlinks", new string[0], convertToLowerInvariant: true).ToList(); Prefabs.Add(this, allowOverriding); this.CalculatePrefabUIntIdentifier(Prefabs); } public float GetTreatmentSuitability(string treatmentIdentifier) { return treatmentSuitability.TryGetValue(treatmentIdentifier, out float suitability) ? suitability : 0.0f; } public PriceInfo GetPriceInfo(Location location) { if (location?.Type == null) { return null; } var locationTypeId = location.Type.Identifier?.ToLowerInvariant(); if (locationPrices != null && locationPrices.ContainsKey(locationTypeId)) { return locationPrices[locationTypeId]; } else { return DefaultPrice; } } public bool CanBeBoughtAtLocation(Location location, out PriceInfo priceInfo) { priceInfo = null; if (location?.Type == null) { return false; } priceInfo = GetPriceInfo(location); return priceInfo != null && priceInfo.CanBeBought && (location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; } public static ItemPrefab Find(string name, string identifier) { ItemPrefab prefab; if (string.IsNullOrEmpty(identifier)) { //legacy support identifier = GenerateLegacyIdentifier(name); } prefab = Find(p => p is ItemPrefab && p.Identifier==identifier) as ItemPrefab; //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)); } if (prefab == null) { DebugConsole.ThrowError("Error loading item - item prefab \"" + name + "\" (identifier \"" + identifier + "\") not found."); } return prefab; } public int? GetMinPrice() { int? minPrice = locationPrices != null && locationPrices.Values.Any() ? locationPrices?.Values.Min(p => p.Price) : null; 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) { Dictionary priceLocations = new Dictionary(); foreach (KeyValuePair locationPrice in locationPrices) { PriceInfo priceInfo = locationPrice.Value; if (priceInfo == null) { continue; } if (!priceInfo.CanBeBought) { continue; } if (priceInfo.Price < maxCost || maxCost == 0) { priceLocations.Add(locationPrice.Key, priceInfo); } } return priceLocations.ToImmutableDictionary(); } public ImmutableDictionary GetSellPricesOver(int minCost = 0, bool sellingImportant = true) { Dictionary priceLocations = new Dictionary(); if (!CanBeSold && sellingImportant) { return priceLocations.ToImmutableDictionary(); } foreach (KeyValuePair locationPrice in locationPrices) { PriceInfo priceInfo = locationPrice.Value; if (priceInfo == null) { continue; } if (priceInfo.Price > minCost) { priceLocations.Add(locationPrice.Key, priceInfo); } } return priceLocations.ToImmutableDictionary(); } public bool IsContainerPreferred(Item item, ItemContainer targetContainer, 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, targetContainer))) { return true; } isSecondary = true; return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, targetContainer)); } public bool IsContainerPreferred(Item item, string[] 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 bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; 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)); private XElement CreateVariantXML(XElement variantElement, ItemPrefab basePrefab) { XElement newElement = new XElement(variantElement.Name); newElement.Add(basePrefab.ConfigElement.Attributes()); newElement.Add(basePrefab.ConfigElement.Elements()); ReplaceElement(newElement, variantElement); void ReplaceElement(XElement element, XElement replacement) { List elementsToRemove = new List(); foreach (XAttribute attribute in replacement.Attributes()) { ReplaceAttribute(element, attribute); } foreach (XElement replacementSubElement in replacement.Elements()) { int index = replacement.Elements().ToList().FindAll(e => e.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)).IndexOf(replacementSubElement); System.Diagnostics.Debug.Assert(index > -1); int i = 0; bool matchingElementFound = false; foreach (XElement subElement in element.Elements()) { if (!subElement.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; } if (i == index) { if (!replacementSubElement.HasAttributes && !replacementSubElement.HasElements) { //if the replacement is empty (no attributes or child elements) //remove the element from the variant elementsToRemove.Add(subElement); } else { ReplaceElement(subElement, replacementSubElement); } matchingElementFound = true; break; } i++; } if (!matchingElementFound) { element.Add(replacementSubElement); } } elementsToRemove.ForEach(e => e.Remove()); } void ReplaceAttribute(XElement element, XAttribute newAttribute) { XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); if (existingAttribute == null) { element.Add(newAttribute); return; } float.TryParse(existingAttribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out float value); if (newAttribute.Value.StartsWith('*')) { string multiplierStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); float.TryParse(multiplierStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float multiplier); if (multiplierStr.Contains('.') || existingAttribute.Value.Contains('.')) { existingAttribute.Value = (value * multiplier).ToString("G", CultureInfo.InvariantCulture); } else { existingAttribute.Value = ((int)(value * multiplier)).ToString(); } } else if (newAttribute.Value.StartsWith('+')) { string additionStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); float.TryParse(additionStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float addition); if (additionStr.Contains('.') || existingAttribute.Value.Contains('.')) { existingAttribute.Value = (value + addition).ToString("G", CultureInfo.InvariantCulture); } else { existingAttribute.Value = ((int)(value + addition)).ToString(); } } else { existingAttribute.Value = newAttribute.Value; } } return newElement; } } }