#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; 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, ContentXElement element) { Prefab = prefab; IncreaseLow = UpgradePrefab.ParsePercentage(element.GetAttributeString("increaselow", string.Empty)!, "IncreaseLow".ToIdentifier(), element, suppressWarnings: prefab.SuppressWarnings); IncreaseHigh = UpgradePrefab.ParsePercentage(element.GetAttributeString("increasehigh", string.Empty)!, "IncreaseHigh".ToIdentifier(), 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 maxLevel = Prefab.MaxLevel; if (level > maxLevel) { maxLevel = level; } int price = BasePrice; price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, level / (float)maxLevel) / 100); return location?.GetAdjustedMechanicalCost(price) ?? price; } } abstract class UpgradeContentPrefab : Prefab { public static readonly PrefabCollection PrefabsAndCategories = new PrefabCollection( onAdd: (prefab, isOverride) => { if (prefab is UpgradePrefab upgradePrefab) { UpgradePrefab.Prefabs.Add(upgradePrefab, isOverride); } else if (prefab is UpgradeCategory upgradeCategory) { UpgradeCategory.Categories.Add(upgradeCategory, isOverride); } }, onRemove: (prefab) => { if (prefab is UpgradePrefab upgradePrefab) { UpgradePrefab.Prefabs.Remove(upgradePrefab); } else if (prefab is UpgradeCategory upgradeCategory) { UpgradeCategory.Categories.Remove(upgradeCategory); } }, onSort: () => { UpgradePrefab.Prefabs.SortAll(); UpgradeCategory.Categories.SortAll(); }, onAddOverrideFile: (file) => { UpgradePrefab.Prefabs.AddOverrideFile(file); UpgradeCategory.Categories.AddOverrideFile(file); }, onRemoveOverrideFile: (file) => { UpgradePrefab.Prefabs.RemoveOverrideFile(file); UpgradeCategory.Categories.RemoveOverrideFile(file); }); public UpgradeContentPrefab(ContentXElement element, UpgradeModulesFile file) : base(file, element) { } } internal class UpgradeCategory : UpgradeContentPrefab { public static readonly PrefabCollection Categories = new PrefabCollection(); private readonly ImmutableHashSet selfItemTags; private readonly HashSet prefabsThatAllowUpgrades = new HashSet(); public readonly bool IsWallUpgrade; public readonly LocalizedString Name; public readonly IEnumerable ItemTags; public UpgradeCategory(ContentXElement element, UpgradeModulesFile file) : base(element, file) { selfItemTags = element.GetAttributeIdentifierArray("items", Array.Empty())?.ToImmutableHashSet() ?? ImmutableHashSet.Empty; Name = element.GetAttributeString("name", string.Empty)!; IsWallUpgrade = element.GetAttributeBool("wallupgrade", false); ItemTags = selfItemTags.CollectionConcat(prefabsThatAllowUpgrades); Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); if (!nameIdentifier.IsEmpty) { Name = TextManager.Get($"{nameIdentifier}"); } else if (Name.IsNullOrWhiteSpace()) { Name = TextManager.Get($"UpgradeCategory.{Identifier}"); } } public void DeterminePrefabsThatAllowUpgrades() { prefabsThatAllowUpgrades.Clear(); prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs .Where(it => it.GetAllowedUpgrades().Contains(Identifier)) .Select(it => it.Identifier)); } public bool CanBeApplied(MapEntity item, UpgradePrefab? upgradePrefab) { if (upgradePrefab != null && item.Submarine is { Info: var info } && !upgradePrefab.IsApplicable(info)) { return false; } bool isStructure = item is Structure; switch (IsWallUpgrade) { case true: return isStructure; case false when isStructure: 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 == tag); } public bool CanBeApplied(XElement element, UpgradePrefab prefab) { if ("Structure" == element.NameAsIdentifier()) { return IsWallUpgrade; } Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); if (identifier.IsEmpty) { return false; } ItemPrefab? item = ItemPrefab.Find(null, identifier); if (item == null) { return false; } Identifier[] disallowedUpgrades = element.GetAttributeIdentifierArray("disallowedupgrades", Array.Empty()); if (disallowedUpgrades.Any(s => s == Identifier || s == prefab.Identifier)) { return false; } return item.GetAllowedUpgrades().Contains(Identifier) || ItemTags.Any(tag => item.Tags.Contains(tag) || item.Identifier == tag); } public static UpgradeCategory? Find(Identifier identifier) { return !identifier.IsEmpty ? Categories.Find(category => category.Identifier == identifier) : null; } public override void Dispose() { } } internal readonly struct UpgradeMaxLevelMod { private enum MaxLevelModType { Invalid, Increase, Set } private readonly Either tierOrClass; private readonly int value; private readonly MaxLevelModType type; public int GetLevelAfter(int level) => type switch { MaxLevelModType.Invalid => level, MaxLevelModType.Increase => level + value, MaxLevelModType.Set => value, _ => throw new ArgumentOutOfRangeException() }; public bool AppliesTo(SubmarineInfo sub) { if (type is MaxLevelModType.Invalid) { return false; } if (tierOrClass.TryGet(out int tier)) { return sub.Tier == tier; } if (tierOrClass.TryGet(out SubmarineClass subClass)) { return sub.SubmarineClass == subClass; } return false; } public UpgradeMaxLevelMod(ContentXElement element) { bool isValid = true; SubmarineClass subClass = element.GetAttributeEnum("class", SubmarineClass.Undefined); int tier = element.GetAttributeInt("tier", 0); if (subClass != SubmarineClass.Undefined) { tierOrClass = subClass; } else { tierOrClass = tier; } string stringValue = element.GetAttributeString("level", null) ?? string.Empty; value = 0; if (string.IsNullOrWhiteSpace(stringValue)) { isValid = false; } char firstChar = stringValue[0]; if (!int.TryParse(stringValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var intValue)) { isValid = false; } value = intValue; if (firstChar.Equals('+') || firstChar.Equals('-')) { type = MaxLevelModType.Increase; } else { type = MaxLevelModType.Set; } if (!isValid) { type = MaxLevelModType.Invalid; } } } internal partial class UpgradePrefab : UpgradeContentPrefab { public static readonly PrefabCollection Prefabs = new PrefabCollection( onAdd: (prefab, isOverride) => { if (!prefab.SuppressWarnings && !isOverride) { foreach (UpgradePrefab matchingPrefab in Prefabs?.Where(p => p != prefab && p.TargetItems.Any(s => prefab.TargetItems.Contains(s))) ?? throw new NullReferenceException("Honestly I have no clue why this could be null...")) { if (matchingPrefab.isOverride) { continue; } var upgradePrefab = matchingPrefab.targetProperties; string key = string.Empty; if (upgradePrefab.Keys.Any(s => prefab.targetProperties.Keys.Any(s1 => s == (key = s1)))) { if (upgradePrefab.ContainsKey(key) && upgradePrefab[key].Any(s => prefab.targetProperties[key].Contains(s))) { DebugConsole.AddWarning($"Upgrade \"{prefab.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."); } } } } }, onRemove: null, onSort: null, onAddOverrideFile: null, onRemoveOverrideFile: null ); private readonly int maxLevel; public int MaxLevel { get { Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub; return sub is { Info: var info } ? GetMaxLevel(info) : maxLevel; } } public LocalizedString Name { get; } public LocalizedString Description { get; } public float IncreaseOnTooltip { get; } private readonly ImmutableHashSet upgradeCategoryIdentifiers; public IEnumerable UpgradeCategories { get { foreach (var id in upgradeCategoryIdentifiers) { if (UpgradeCategory.Categories.TryGet(id, out var category)) { yield return category!; } } } } public UpgradePrice Price { get; } private bool isOverride => Prefabs.IsOverride(this); public ContentXElement SourceElement { get; } 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 readonly ImmutableArray MaxLevelsMods; public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file) { Name = element.GetAttributeString("name", string.Empty)!; Description = element.GetAttributeString("description", string.Empty)!; maxLevel = element.GetAttributeInt("maxlevel", 1); SuppressWarnings = element.GetAttributeBool("supresswarnings", false); HideInMenus = element.GetAttributeBool("hideinmenus", false); SourceElement = element; var targetProperties = new Dictionary(); var maxLevels = new List(); Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); if (!nameIdentifier.IsEmpty) { Name = TextManager.Get($"UpgradeName.{nameIdentifier}"); } else if (Name.IsNullOrWhiteSpace()) { Name = TextManager.Get($"UpgradeName.{Identifier}"); } Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); if (!descriptionIdentifier.IsEmpty) { Description = TextManager.Get($"UpgradeDescription.{descriptionIdentifier}"); } else if (Description.IsNullOrWhiteSpace()) { Description = TextManager.Get($"UpgradeDescription.{Identifier}"); } IncreaseOnTooltip = element.GetAttributeFloat("increaseontooltip", 0f); DebugConsole.Log(" " + Name); #if CLIENT var decorativeSprites = new List(); #endif foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "price": { Price = new UpgradePrice(this, subElement); break; } case "maxlevel": { maxLevels.Add(new UpgradeMaxLevelMod(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; } } } #if CLIENT DecorativeSprites = decorativeSprites.ToImmutableArray(); #endif this.targetProperties = targetProperties; MaxLevelsMods = maxLevels.ToImmutableArray(); upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty())? .ToImmutableHashSet() ?? ImmutableHashSet.Empty; } public int GetMaxLevel(SubmarineInfo info) { int level = maxLevel; foreach (UpgradeMaxLevelMod mod in MaxLevelsMods) { if (mod.AppliesTo(info)) { level = mod.GetLevelAfter(level); } } return level; } public bool IsApplicable(SubmarineInfo? info) { if (info is null) { return false; } return GetMaxLevel(info) > 0; } public bool IsDisallowed(MapEntity item) { return item.DisallowedUpgradeSet.Contains(Identifier) || UpgradeCategories.Any(c => item.DisallowedUpgradeSet.Contains(c.Identifier)); } public static UpgradePrefab? Find(Identifier identifier) { return identifier != Identifier.Empty ? Prefabs.Find(prefab => prefab.Identifier == identifier) : null; } /// /// 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, Identifier attribute = default, XElement? sourceElement = null, bool suppressWarnings = false) { string? line = sourceElement?.ToString().Split('\n')[0].Trim(); bool doWarnings = !suppressWarnings && !attribute.IsEmpty && 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; } public override void Dispose() { #if CLIENT Sprite?.Remove(); Sprite = null; DecorativeSprites.ForEach(sprite => sprite.Remove()); targetProperties.Clear(); #endif } } }