#nullable enable using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; namespace Barotrauma { internal readonly struct UpgradePrice { public readonly int BasePrice; public readonly int IncreaseLow; public readonly int IncreaseHigh; public readonly UpgradePrefab Prefab; public UpgradePrice(UpgradePrefab prefab, XElement element) { Prefab = prefab; IncreaseLow = UpgradePrefab.ParsePercentage(element.GetAttributeString("increaselow", string.Empty), "IncreaseLow", element, suppressWarnings: prefab.SuppressWarnings); IncreaseHigh = UpgradePrefab.ParsePercentage(element.GetAttributeString("increasehigh", string.Empty), "IncreaseHigh", element, suppressWarnings: prefab.SuppressWarnings); BasePrice = element.GetAttributeInt("baseprice", -1); if (BasePrice == -1) { if (prefab.SuppressWarnings) { DebugConsole.AddWarning($"Price attribute \"baseprice\" is not defined for {prefab?.Identifier}.\n " + "The value has been assumed to be '1000'."); BasePrice = 1000; } } } public int GetBuyprice(int level, Location? location = null) { int price = BasePrice; for (int i = 1; i <= level; i++) { price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, i / (float)Prefab.MaxLevel) / 100); } return location?.GetAdjustedMechanicalCost(price) ?? price; } } internal class UpgradeCategory { public static readonly List Categories = new List(); public readonly string[] ItemTags; public readonly string Identifier; public readonly bool IsWallUpgrade; public readonly string Name; public UpgradeCategory(XElement element) { ItemTags = element.GetAttributeStringArray("items", new string[] { }); Identifier = element.GetAttributeString("identifier", string.Empty); Name = element.GetAttributeString("name", string.Empty); IsWallUpgrade = element.GetAttributeBool("wallupgrade", false); string nameIdentifier = element.GetAttributeString("nameidentifier", ""); if (!string.IsNullOrWhiteSpace(nameIdentifier)) { Name = TextManager.Get($"{nameIdentifier}", returnNull: true) ?? string.Empty; } else if (string.IsNullOrWhiteSpace(Name)) { Name = TextManager.Get($"UpgradeCategory.{Identifier}", true) ?? string.Empty; } foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { string[] identifierArray = itemPrefab.AllowedUpgrades.Split(","); if (identifierArray.Contains(Identifier)) { ItemTags = ItemTags.Concat(new[] { itemPrefab.Identifier }).ToArray(); } } Categories.Add(this); } public bool CanBeApplied(Item item, UpgradePrefab? upgradePrefab) { if (IsWallUpgrade) { return false; } if (upgradePrefab != null && upgradePrefab.IsDisallowed(item)) { return false; } return item.prefab.GetAllowedUpgrades().Contains(Identifier) || ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier.Equals(tag, StringComparison.OrdinalIgnoreCase)); } public bool CanBeApplied(XElement element, UpgradePrefab prefab) { if (string.Equals("Structure", element.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { return IsWallUpgrade; } string identifier = element.GetAttributeString("identifier", string.Empty); if (string.IsNullOrWhiteSpace(identifier)) { return false; } ItemPrefab? item = ItemPrefab.Find(null, identifier); if (item == null) { return false; } string[] disallowedUpgrades = element.GetAttributeStringArray("disallowedupgrades", new string[0]); if (disallowedUpgrades.Any(s => s.Equals(Identifier, StringComparison.OrdinalIgnoreCase) || s.Equals(prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return false; } return item.GetAllowedUpgrades().Contains(Identifier) || ItemTags.Any(tag => item.Tags.Contains(tag) || item.Identifier.Equals(tag, StringComparison.OrdinalIgnoreCase)); } public static UpgradeCategory? Find(string idenfitier) { return !string.IsNullOrWhiteSpace(idenfitier) ? Categories.Find(category => string.Equals(category.Identifier, idenfitier, StringComparison.OrdinalIgnoreCase)) : null; } } internal partial class UpgradePrefab : IPrefab, IDisposable { public static readonly PrefabCollection Prefabs = new PrefabCollection(); public int MaxLevel { get; } public string OriginalName { get; } public string Name { get; } public string Description { get; } public float IncreaseOnTooltip { get; } public string Identifier { get; } public string FilePath { get; } public UpgradeCategory[] UpgradeCategories { get; } public UpgradePrice Price { get; } public ContentPackage? ContentPackage { get; private set; } private bool IsOverride { get; } public XElement SourceElement { get; } private bool Disposed { get; set; } public bool SuppressWarnings { get; } public bool HideInMenus { get; } public IEnumerable TargetItems => UpgradeCategories.SelectMany(u => u.ItemTags); public bool IsWallUpgrade => UpgradeCategories.All(u => u.IsWallUpgrade); private Dictionary TargetProperties { get; } private UpgradePrefab(XElement element, string filePath, bool isOverride) { Name = element.GetAttributeString("name", string.Empty); Description = element.GetAttributeString("description", string.Empty); MaxLevel = element.GetAttributeInt("maxlevel", 1); Identifier = element.GetAttributeString("identifier", ""); SuppressWarnings = element.GetAttributeBool("supresswarnings", false); HideInMenus = element.GetAttributeBool("hideinmenus", false); FilePath = filePath; SourceElement = element; IsOverride = isOverride; OriginalName = Name; var targetProperties = new Dictionary(); string nameIdentifier = element.GetAttributeString("nameidentifier", ""); if (!string.IsNullOrWhiteSpace(nameIdentifier)) { Name = TextManager.Get($"UpgradeName.{nameIdentifier}", returnNull: true) ?? string.Empty; } else if (string.IsNullOrWhiteSpace(Name)) { Name = TextManager.Get($"UpgradeName.{Identifier}", returnNull: true) ?? string.Empty; } if (string.IsNullOrWhiteSpace(Description)) { Description = TextManager.Get($"UpgradeDescription.{Identifier}", returnNull: true) ?? string.Empty; } IncreaseOnTooltip = element.GetAttributeFloat("increaseontooltip", 0f); DebugConsole.Log(" " + Name); foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "price": { Price = new UpgradePrice(this, subElement); break; } #if CLIENT case "decorativesprite": { DecorativeSprites.Add(new DecorativeSprite(subElement)); break; } case "sprite": { Sprite = new Sprite(subElement); break; } #else case "decorativesprite": case "sprite": break; #endif default: { IEnumerable properties = subElement.Attributes().Select(attribute => attribute.Name.ToString()); targetProperties.Add(subElement.Name.ToString(), properties.ToArray()); break; } } } TargetProperties = targetProperties; string[] categories = element.GetAttributeStringArray("categories", new string[] { }); UpgradeCategories = (from category in UpgradeCategory.Categories from identifier in categories where string.Equals(category.Identifier, identifier) select category).ToArray(); if (!SuppressWarnings && !IsOverride) { foreach (UpgradePrefab matchingPrefab in Prefabs.Where(prefab => prefab.TargetItems.Any(s => TargetItems.Contains(s)))) { if (matchingPrefab.IsOverride) { continue; } var upgradePrefab = matchingPrefab.TargetProperties; string key = string.Empty; if (upgradePrefab.Keys.Any(s => TargetProperties.Keys.Any(s1 => s == (key = s1)))) { if (upgradePrefab.ContainsKey(key) && upgradePrefab[key].Any(s => TargetProperties[key].Contains(s))) { DebugConsole.AddWarning($"Upgrade \"{Identifier}\" is affecting a property that is also being affected by \"{matchingPrefab.Identifier}\".\n" + "This is unsupported and might yield unexpected results if both upgrades are applied at the same time to the same item.\n" + "Add the attribute suppresswarnings=\"true\" to your XML element to disable this warning if you know what you're doing."); } } } } Prefabs.Add(this, isOverride); } public bool IsDisallowed(Item item) { return item.disallowedUpgrades.Contains(Identifier) || UpgradeCategories.Any(c => item.disallowedUpgrades.Contains(c.Identifier)); } public static UpgradePrefab? Find(string identifier) { return !string.IsNullOrWhiteSpace(identifier) ? Prefabs.Find(prefab => prefab.Identifier == identifier) : null; } public static void LoadAll(IEnumerable files) { DebugConsole.Log("Loading upgrade module prefabs: "); foreach (ContentFile file in files) { LoadFromFile(file); } } private static void LoadFromFile(ContentFile file) { XDocument doc = XMLExtensions.TryLoadXml(file.Path); var rootElement = doc?.Root; if (rootElement == null) { return; } switch (rootElement.Name.ToString().ToLowerInvariant()) { case "upgrademodule": { new UpgradePrefab(rootElement, file.Path, false) { ContentPackage = file.ContentPackage }; break; } case "upgradecategory": { new UpgradeCategory(rootElement); break; } case "upgrademodules": { foreach (var element in rootElement.Elements()) { if (element.IsOverride()) { var upgradeElement = element.GetChildElement("upgradeprefab"); if (upgradeElement != null) { new UpgradePrefab(upgradeElement, file.Path, true) { ContentPackage = file.ContentPackage }; } else { DebugConsole.ThrowError($"Cannot find an upgrade element from the children of the override element defined in {file.Path}"); } } else { switch (element.Name.ToString().ToLowerInvariant()) { case "upgrademodule": { new UpgradePrefab(element, file.Path, false) { ContentPackage = file.ContentPackage }; break; } case "upgradecategory": { new UpgradeCategory(element); break; } } } } break; } case "override": { var upgrades = rootElement.GetChildElement("upgrademodules"); if (upgrades != null) { foreach (var element in upgrades.Elements()) { new UpgradePrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; } } foreach (var element in rootElement.GetChildElements("upgrademodule")) { new UpgradePrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; } break; } default: DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}\n " + "Valid elements are: \"UpgradeModule\", \"UpgradeModules\" and \"Override\"."); break; } } /// /// Parse a integer value from a string that is formatted like a percentage increase / decrease. /// /// String to parse /// What XML attribute the value originates from, only used for warning formatting. /// What XMLElement the value originates from, only used for warning formatting. /// Whether or not to suppress warnings if both "attribute" and "sourceElement" are defined. /// /// /// This sample returns -15 as an integer. /// /// XElement element = new XElement("change", new XAttribute("increase", "-15%")); /// ParsePercentage(element.GetAttributeString("increase", string.Empty)); /// /// public static int ParsePercentage(string value, string? attribute = null, XElement? sourceElement = null, bool suppressWarnings = false) { string? line = sourceElement?.ToString().Split('\n')[0].Trim(); bool doWarnings = !suppressWarnings && attribute != null && sourceElement != null && line != null; if (string.IsNullOrWhiteSpace(value)) { if (doWarnings) { DebugConsole.AddWarning($"Attribute \"{attribute}\" not found at {sourceElement!.Document?.ParseContentPathFromUri()} @ '{line}'.\n " + "Value has been assumed to be '0'."); } return 1; } if (!int.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var price)) { string str = value; if (str.Length > 1 && str[0] == '+') { str = str.Substring(1); } if (str.Length > 1 && str[^1] == '%') { str = str.Substring(0, str.Length - 1); } if (int.TryParse(str, out price)) { return price; } } else { return price; } if (doWarnings) { DebugConsole.AddWarning($"Value in attribute \"{attribute}\" is not formatted correctly\n " + $"at {sourceElement!.Document?.ParseContentPathFromUri()} @ '{line}'.\n " + "It should be an integer with optionally a '+' or '-' at the front and/or '%' at the end.\n" + "The value has been assumed to be '0'."); } return 1; } private void Dispose(bool disposing) { if (!Disposed) { if (disposing) { Prefabs.Remove(this); #if CLIENT Sprite.Remove(); Sprite = null; DecorativeSprites.ForEach(sprite => sprite.Remove()); DecorativeSprites.Clear(); TargetProperties.Clear(); #endif } } Disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } }