Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs
2025-09-17 13:44:21 +03:00

451 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
/// <summary>
/// Used by various features to define different kinds of relations between items:
/// for example, which item a character must have equipped to interact with some item in some way,
/// which items can go inside a container, or which kind of item the target of a status effect must have for the effect to execute.
/// </summary>
class RelatedItem
{
public enum RelationType
{
None,
/// <summary>
/// The item must be contained inside the item this relation is defined in.
/// Can for example by used to make an item usable only when there's a specific kind of item inside it.
/// </summary>
Contained,
/// <summary>
/// The user must have equipped the item (i.e. held or worn).
/// </summary>
Equipped,
/// <summary>
/// The user must have picked up the item (i.e. the item needs to be in the user's inventory).
/// </summary>
Picked,
/// <summary>
/// The item this relation is defined in must be inside a specific kind of container.
/// Can for example by used to make an item do something when it's inside some other type of item.
/// </summary>
Container,
/// <summary>
/// Signifies an error (type could not be parsed)
/// </summary>
Invalid
}
/// <summary>
/// Should an empty inventory (or an empty inventory slot if <see cref="TargetSlot"/> is set) be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it.
/// </summary>
public bool MatchOnEmpty { get; set; }
/// <summary>
/// Should only an empty inventory (or an empty inventory slot if <see cref="TargetSlot"/> is set) be considered valid? Can be used to, for example, make an item do something when there's nothing inside it.
/// </summary>
public bool RequireEmpty { get; set; }
private bool RequireOrMatchOnEmpty => MatchOnEmpty || RequireEmpty;
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. Can be used to ignore the requirement in the submarine editor,
/// making it easier to for example make rewire things that require some special tool to rewire.
/// </summary>
public bool IgnoreInEditor { get; set; }
/// <summary>
/// Identifier(s) or tag(s) of the items that are NOT considered valid.
/// Can be used to, for example, exclude some specific items when using tags that apply to multiple items.
/// </summary>
public ImmutableHashSet<Identifier> ExcludedIdentifiers { get; private set; }
private readonly RelationType type;
public List<StatusEffect> StatusEffects = new List<StatusEffect>();
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. A message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel).
/// </summary>
public LocalizedString Msg;
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. The localization tag of a message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel).
/// </summary>
public Identifier MsgTag;
/// <summary>
/// Should broken (0 condition) items be excluded?
/// </summary>
public bool ExcludeBroken { get; private set; }
/// <summary>
/// Should full condition (100%) items be excluded?
/// </summary>
public bool ExcludeFullCondition { get; private set; }
/// <summary>
/// Are item variants considered valid?
/// </summary>
public bool AllowVariants { get; private set; } = true;
public RelationType Type
{
get { return type; }
}
/// <summary>
/// Index of the slot the target must be in when targeting a Contained item or a character inventory.
/// </summary>
public int TargetSlot = -1;
/// <summary>
/// The slot type the target must be in when targeting an item contained inside a character's inventory.
/// </summary>
public InvSlotType CharacterInventorySlotType;
/// <summary>
/// Overrides the position defined in ItemContainer. Only valid when used in the Containable definitions of an ItemContainer.
/// </summary>
public Vector2? ItemPos;
/// <summary>
/// Only valid when used in the Containable definitions of an ItemContainer.
/// Only affects when ItemContainer.hideItems is false. Doesn't override the value.
/// </summary>
public bool Hide;
/// <summary>
/// Only valid when used in the Containable definitions of an ItemContainer.
/// Can be used to override the rotation of specific items in the container.
/// </summary>
public float Rotation;
/// <summary>
/// Only valid when used in the Containable definitions of an ItemContainer.
/// Can be used to force specific items to stay active inside the container (such as flashlights attached to a gun).
/// </summary>
public bool SetActive;
/// <summary>
/// Only valid when used in the Containable definitions of an ItemContainer.
/// Should the character who equipped the item be blamed if the wearer / character who's inventory the item is in dies?
/// </summary>
public bool BlameEquipperForDeath;
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. Can be used to make the requirement optional,
/// meaning that you don't need to have the item to interact with something, but having it may still affect what the interaction does (such as using a crowbar on a door).
/// </summary>
public bool IsOptional { get; set; }
public string JoinedIdentifiers
{
get => Identifiers.ConvertToString();
set => Identifiers = value.ToIdentifiers().ToImmutableHashSet();
}
/// <summary>
/// Identifier(s) or tag(s) of the items that are considered valid.
/// </summary>
public ImmutableHashSet<Identifier> Identifiers { get; private set; }
public string JoinedExcludedIdentifiers
{
get => ExcludedIdentifiers.ConvertToString();
set => ExcludedIdentifiers = value.ToIdentifiers().ToImmutableHashSet();
}
public bool MatchesItem(Item item)
{
if (item == null) { return false; }
if (ExcludedIdentifiers.Contains(item.Prefab.Identifier)) { return false; }
foreach (var excludedIdentifier in ExcludedIdentifiers)
{
if (item.HasTag(excludedIdentifier)) { return false; }
}
if (item.ParentInventory?.Owner is Character character && CharacterInventorySlotType != InvSlotType.None)
{
if (!character.HasEquippedItem(item, CharacterInventorySlotType)) { return false; }
}
if (Identifiers.Contains(item.Prefab.Identifier)) { return true; }
foreach (var identifier in Identifiers)
{
if (item.HasTag(identifier)) { return true; }
}
if (AllowVariants && !item.Prefab.VariantOf.IsEmpty)
{
if (Identifiers.Contains(item.Prefab.VariantOf)) { return true; }
}
return false;
}
public bool MatchesItem(ItemPrefab itemPrefab)
{
if (itemPrefab == null) { return false; }
if (ExcludedIdentifiers.Contains(itemPrefab.Identifier)) { return false; }
foreach (var excludedIdentifier in ExcludedIdentifiers)
{
if (itemPrefab.Tags.Contains(excludedIdentifier)) { return false; }
}
if (Identifiers.Contains(itemPrefab.Identifier)) { return true; }
foreach (var identifier in Identifiers)
{
if (itemPrefab.Tags.Contains(identifier)) { return true; }
}
if (AllowVariants && !itemPrefab.VariantOf.IsEmpty)
{
if (Identifiers.Contains(itemPrefab.VariantOf)) { return true; }
}
return false;
}
public RelatedItem(Identifier[] identifiers, Identifier[] excludedIdentifiers)
{
this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet();
this.ExcludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet();
}
public RelatedItem(ContentXElement element, string parentDebugName)
{
Identifier[] identifiers;
if (element.GetAttribute("name") != null)
{
//backwards compatibility + a console warning
DebugConsole.ThrowError($"Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names.", contentPackage: element.ContentPackage);
Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty<Identifier>());
//attempt to convert to identifiers and tags
List<Identifier> convertedIdentifiers = new List<Identifier>();
foreach (Identifier itemName in itemNames)
{
var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName.Value);
if (matchingItem != null)
{
convertedIdentifiers.Add(matchingItem.Identifier);
}
else
{
//no matching item found, this must be a tag
convertedIdentifiers.Add(itemName);
}
}
identifiers = convertedIdentifiers.ToArray();
}
else
{
identifiers = element.GetAttributeIdentifierArray("items", null) ?? element.GetAttributeIdentifierArray("item", null);
if (identifiers == null)
{
identifiers = element.GetAttributeIdentifierArray("identifiers", null) ?? element.GetAttributeIdentifierArray("tags", null);
if (identifiers == null)
{
identifiers = element.GetAttributeIdentifierArray("identifier", null) ?? element.GetAttributeIdentifierArray("tag", Array.Empty<Identifier>());
}
}
}
this.Identifiers = identifiers.ToImmutableHashSet();
Identifier[] excludedIdentifiers = element.GetAttributeIdentifierArray("excludeditems", null) ?? element.GetAttributeIdentifierArray("excludeditem", null);
if (excludedIdentifiers == null)
{
excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifiers", null) ?? element.GetAttributeIdentifierArray("excludedtags", null);
if (excludedIdentifiers == null)
{
excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifier", null) ?? element.GetAttributeIdentifierArray("excludedtag", Array.Empty<Identifier>());
}
}
this.ExcludedIdentifiers = excludedIdentifiers.ToImmutableHashSet();
ExcludeBroken = element.GetAttributeBool("excludebroken", true);
RequireEmpty = element.GetAttributeBool("requireempty", false);
ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false);
AllowVariants = element.GetAttributeBool("allowvariants", true);
Rotation = element.GetAttributeFloat("rotation", 0f);
SetActive = element.GetAttributeBool("setactive", false);
BlameEquipperForDeath = element.GetAttributeBool(nameof(BlameEquipperForDeath), false);
CharacterInventorySlotType = element.GetAttributeEnum(nameof(CharacterInventorySlotType), InvSlotType.None);
if (element.GetAttribute(nameof(Hide)) != null)
{
Hide = element.GetAttributeBool(nameof(Hide), false);
}
if (element.GetAttribute(nameof(ItemPos)) != null)
{
ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero);
}
string typeStr = element.GetAttributeString("type", "");
if (string.IsNullOrEmpty(typeStr))
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "containable":
typeStr = "Contained";
break;
case "suitablefertilizer":
case "suitableseed":
typeStr = "None";
break;
}
}
if (!Enum.TryParse(typeStr, true, out type))
{
DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type.", contentPackage: element.ContentPackage);
type = RelationType.Invalid;
}
MsgTag = element.GetAttributeIdentifier("msg", Identifier.Empty);
LocalizedString msg = TextManager.Get(MsgTag);
if (!msg.Loaded)
{
Msg = MsgTag.Value;
}
else
{
#if CLIENT
foreach (InputType inputType in Enum.GetValues(typeof(InputType)))
{
string inputTag = $"[{inputType.ToString().ToLowerInvariant()}]";
if (!msg.Contains(inputTag)) { continue; }
msg = msg.Replace(inputTag, GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType));
}
Msg = msg;
#endif
}
foreach (var subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; }
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
}
IsOptional = element.GetAttributeBool("optional", false);
IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false);
MatchOnEmpty = element.GetAttributeBool("matchonempty", false);
TargetSlot = element.GetAttributeInt("targetslot", -1);
}
public bool CheckRequirements(Character character, Item parentItem)
{
switch (type)
{
case RelationType.Contained:
if (parentItem == null) { return false; }
return CheckContained(parentItem);
case RelationType.Container:
if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty || RequireEmpty; }
return CheckItem(parentItem.Container, this);
case RelationType.Equipped:
if (character == null) { return false; }
foreach (var item in character.Inventory.AllItemsMod)
{
if (character.HasEquippedItem(item) && CheckItem(item, this))
{
if (RequireEmpty && item.Condition > 0) { return false; }
return true;
}
}
//got this far -> no matching item was equipped
//return true if we require or want to match "empty" (no matching item), otherwise false
return RequireOrMatchOnEmpty;
case RelationType.Picked:
if (character == null) { return false; }
if (character.Inventory == null) { return MatchOnEmpty || RequireEmpty; }
var allItems = TargetSlot == -1 ? character.Inventory.AllItems : character.Inventory.GetItemsAt(TargetSlot);
if (RequireOrMatchOnEmpty && allItems.None()) { return true; }
foreach (Item pickedItem in allItems)
{
if (pickedItem == null) { continue; }
if (CheckItem(pickedItem, this))
{
if (RequireEmpty && pickedItem.Condition > 0) { return false; }
return true;
}
}
break;
default:
return true;
}
static bool CheckItem(Item i, RelatedItem ri) => (!ri.ExcludeBroken || ri.RequireEmpty || i.Condition > 0.0f) && (!ri.ExcludeFullCondition || !i.IsFullCondition) && ri.MatchesItem(i);
return false;
}
private bool CheckContained(Item parentItem)
{
if (parentItem.OwnInventory == null) { return false; }
if (TargetSlot == -1 && RequireOrMatchOnEmpty)
{
bool isEmpty = parentItem.OwnInventory.IsEmpty();
if (RequireEmpty) { return isEmpty; }
if (MatchOnEmpty && isEmpty) { return true; }
}
foreach (var container in parentItem.GetComponents<Items.Components.ItemContainer>())
{
if (TargetSlot > -1 && RequireOrMatchOnEmpty)
{
var itemInSlot = container.Inventory.GetItemAt(TargetSlot);
if (RequireEmpty) { return itemInSlot == null; }
if (MatchOnEmpty && itemInSlot == null) { return true; }
}
foreach (Item contained in container.Inventory.AllItems)
{
if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; }
if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; }
if (CheckContained(contained)) { return true; }
}
}
return false;
}
public void Save(XElement element)
{
element.Add(
new XAttribute("items", JoinedIdentifiers),
new XAttribute("type", type.ToString()),
new XAttribute("characterinventoryslottype", CharacterInventorySlotType.ToString()),
new XAttribute("optional", IsOptional),
new XAttribute("ignoreineditor", IgnoreInEditor),
new XAttribute("excludebroken", ExcludeBroken),
new XAttribute("requireempty", RequireEmpty),
new XAttribute("excludefullcondition", ExcludeFullCondition),
new XAttribute("targetslot", TargetSlot),
new XAttribute("allowvariants", AllowVariants),
new XAttribute("rotation", Rotation),
new XAttribute("setactive", SetActive));
if (Hide)
{
element.Add(new XAttribute(nameof(Hide), true));
}
if (ItemPos.HasValue)
{
element.Add(new XAttribute(nameof(ItemPos), ItemPos.Value));
}
if (ExcludedIdentifiers.Count > 0)
{
element.Add(new XAttribute("excludedidentifiers", JoinedExcludedIdentifiers));
}
if (!Msg.IsNullOrWhiteSpace()) { element.Add(new XAttribute("msg", MsgTag.IsEmpty ? Msg : MsgTag.Value)); }
}
public static RelatedItem Load(ContentXElement element, bool returnEmpty, string parentDebugName)
{
RelatedItem ri = new RelatedItem(element, parentDebugName);
if (ri.Type == RelationType.Invalid) { return null; }
if (ri.Identifiers.None() && ri.ExcludedIdentifiers.None() && !returnEmpty) { return null; }
return ri;
}
}
}