Files
2025-09-17 13:44:21 +03:00

752 lines
31 KiB
C#

#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 Barotrauma.Items.Components;
namespace Barotrauma
{
/// <summary>
/// Conditionals are used by some in-game mechanics to require one
/// or more conditions to be met for those mechanics to be active.
/// For example, some StatusEffects use Conditionals to only trigger
/// if the affected character is alive.
/// </summary>
sealed class PropertyConditional
{
// TODO: Make this testable and add tests
/// <summary>
/// Category of properties to check against
/// </summary>
public enum ConditionType
{
/// <summary>
/// If there exists an affliction with an identifier that matches the given key, check against the strength of that affliction.
/// Otherwise, check against the value of one of the target's properties.
///
/// The target object's available properties depend on how that object is defined in the [source code](https://github.com/Regalis11/Barotrauma).
///
/// This is not applicable if the element contains the attribute
/// `SkillRequirement="true"`.
/// </summary>
/// <AutoDocEntryName value="Property or affliction identifier" />
/// <example>
/// <Conditional WatchersGaze="gt 0" /> <!-- there is an affliction with identifier WatchersGaze -> check for that affliction -->
/// <Conditional IsDead="true" /> <!-- there is no affliction with identifier IsDead -> check property instead -->
/// </example>
PropertyValueOrAffliction,
/// <summary>
/// Check against the target character's skill with the same name as the attribute.
///
/// This is only applicable if the element contains the attribute
/// `SkillRequirement="true"`.
/// </summary>
/// <AutoDocEntryName value="Skill identifier" />
/// <example>
/// <Conditional SkillRequirement="true" weapons="lt 50" />
/// </example>
SkillRequirement,
/// <summary>
/// Check against the name of the target.
/// </summary>
Name,
/// <summary>
/// Check against the species identifier of the target. Only works on characters.
/// </summary>
SpeciesName,
/// <summary>
/// Check against the species group of the target. Only works on characters.
/// </summary>
SpeciesGroup,
/// <summary>
/// Check against the target's tags. Only works on items and characters.
///
/// Several tags can be checked against by using a comma-separated list.
/// </summary>
HasTag,
/// <summary>
/// Check against the tags of the target's active status effects.
///
/// Several tags can be checked against by using a comma-separated list.
/// </summary>
HasStatusTag,
/// <summary>
/// Check against the target's specifier tags. In the vanilla game, these are the head index
/// and gender. See human.xml for more details.
///
/// Several tags can be checked against by using a comma-separated list.
/// </summary>
HasSpecifierTag,
/// <summary>
/// Check against the target's entity type.
///
/// The currently supported values are "character", "limb", "item", "structure" and "null".
/// </summary>
EntityType,
/// <summary>
/// Check against the target's limb type. See <see cref="Barotrauma.LimbType"/>.
/// </summary>
LimbType,
/// <summary>
/// Check against the current World Hostility setting (previously known as "Difficulty").
/// </summary>
WorldHostility,
/// <summary>
/// Check against the difficulty of the current level.
/// </summary>
LevelDifficulty
}
public enum LogicalOperatorType
{
And,
Or
}
/// <summary>
/// There are several ways to compare properties to values.
/// The comparison operator to use can be specified by placing one of the following before the value to compare against.
/// </summary>
public enum ComparisonOperatorType
{
None,
/// <summary>
/// Require that the property being checked equals the given value.
///
/// This is the default operator used if none is specified.
/// </summary>
Equals,
/// <summary>
/// Require that the property being checked doesn't equal the given value.
/// </summary>
NotEquals,
/// <summary>
/// Require that the property being checked is less than the given value.
///
/// This can only be used to compare with numeric object properties,
/// affliction strengths and skill levels.
/// </summary>
LessThan,
/// <summary>
/// Require that the property being checked is less than or equal to the given value.
///
/// This can only be used to compare with numeric object properties,
/// affliction strengths and skill levels.
/// </summary>
LessThanEquals,
/// <summary>
/// Require that the property being checked is greater than the given value.
///
/// This can only be used to compare with numeric object properties,
/// affliction strengths and skill levels.
/// </summary>
GreaterThan,
/// <summary>
/// Require that the property being checked is greater than or equal to the given value.
///
/// This can only be used to compare with numeric object properties,
/// affliction strengths and skill levels.
/// </summary>
GreaterThanEquals
}
public readonly ConditionType Type;
public readonly ComparisonOperatorType ComparisonOperator;
public readonly Identifier AttributeName;
public readonly string AttributeValue;
public readonly ImmutableArray<Identifier> AttributeValueAsTags;
public readonly float? FloatValue;
private readonly WorldHostilityOption cachedHostilityValue;
/// <summary>
/// If set to the name of one of the target's ItemComponents, the conditionals defined by this element check against the properties of that component.
/// Only works on items.
/// </summary>
public readonly string TargetItemComponent;
/// <summary>
/// When targeting item components, should we require them all to match the conditional or any (default).
/// </summary>
public readonly LogicalOperatorType ItemComponentComparison;
/// <summary>
/// If set to true, the conditionals defined by this element check against the attacking character instead of the attacked character.
/// Only applies to a character's attacks and targeting parameters.
/// </summary>
public readonly bool TargetSelf;
/// <summary>
/// If set to true, the conditionals defined by this element check against the entity containing the target.
/// </summary>
public readonly bool TargetContainer;
/// <summary>
/// If this and TargetContainer are set to true, the conditionals defined by this element check against the entity containing the target's container.
/// For example, diving suits have a status effect that targets contained oxygen tanks, with a conditional that only passes if the locker containing the suit is powered.
/// </summary>
public readonly bool TargetGrandParent;
/// <summary>
/// If set to true, the conditionals defined by this element check against the items contained by the target. Only works with items.
/// </summary>
public readonly bool TargetContainedItem;
public static IEnumerable<PropertyConditional> FromXElement(ContentXElement element, Predicate<XAttribute>? predicate = null)
{
string targetItemComponent = element.GetAttributeString(nameof(TargetItemComponent), string.Empty);
bool targetContainer = element.GetAttributeBool(nameof(TargetContainer), false);
bool targetSelf = element.GetAttributeBool(nameof(TargetSelf), false);
bool targetGrandParent = element.GetAttributeBool(nameof(TargetGrandParent), false);
bool targetContainedItem = element.GetAttributeBool(nameof(TargetContainedItem), false);
LogicalOperatorType itemComponentComparison = element.GetAttributeEnum(nameof(ItemComponentComparison), LogicalOperatorType.Or);
ConditionType? overrideConditionType = null;
if (element.GetAttributeBool(nameof(ConditionType.SkillRequirement), false))
{
overrideConditionType = ConditionType.SkillRequirement;
}
foreach (var attribute in element.Attributes())
{
if (!IsValid(attribute)) { continue; }
if (predicate != null && !predicate(attribute)) { continue; }
(ComparisonOperatorType comparisonOperator, string attributeValueString) = ExtractComparisonOperatorFromConditionString(attribute.Value);
if (string.IsNullOrWhiteSpace(attributeValueString))
{
DebugConsole.ThrowError($"Conditional attribute value is empty: {element}", contentPackage: element.ContentPackage);
continue;
}
ConditionType conditionType = overrideConditionType ??
(Enum.TryParse(attribute.Name.LocalName, ignoreCase: true, out ConditionType type) ? type : ConditionType.PropertyValueOrAffliction);
yield return new PropertyConditional(
attributeName: attribute.NameAsIdentifier(),
comparisonOperator: comparisonOperator,
attributeValue: attributeValueString,
targetItemComponent: targetItemComponent,
itemComponentComparison: itemComponentComparison,
targetSelf: targetSelf,
targetContainer: targetContainer,
targetGrandParent: targetGrandParent,
targetContainedItem: targetContainedItem,
conditionType: conditionType);
}
}
private static bool IsValid(XAttribute attribute)
{
switch (attribute.Name.ToString().ToLowerInvariant())
{
case "targetitemcomponent":
case "targetself":
case "targetcontainer":
case "targetgrandparent":
case "targetcontaineditem":
case "skillrequirement":
case "targetslot":
return false;
default:
return true;
}
}
private PropertyConditional(
Identifier attributeName,
ComparisonOperatorType comparisonOperator,
string attributeValue,
string targetItemComponent,
LogicalOperatorType itemComponentComparison,
bool targetSelf,
bool targetContainer,
bool targetGrandParent,
bool targetContainedItem,
ConditionType conditionType)
{
AttributeName = attributeName;
TargetItemComponent = targetItemComponent;
ItemComponentComparison = itemComponentComparison;
TargetSelf = targetSelf;
TargetContainer = targetContainer;
TargetGrandParent = targetGrandParent;
TargetContainedItem = targetContainedItem;
Type = conditionType;
ComparisonOperator = comparisonOperator;
AttributeValue = attributeValue;
AttributeValueAsTags = AttributeValue.Split(',')
.Select(s => s.ToIdentifier())
.ToImmutableArray();
if (float.TryParse(AttributeValue, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
{
FloatValue = value;
}
if (Type == ConditionType.WorldHostility && Enum.TryParse(AttributeValue, ignoreCase: true, out WorldHostilityOption hostilityValue))
{
cachedHostilityValue = hostilityValue;
}
}
public static (ComparisonOperatorType ComparisonOperator, string ConditionStr) ExtractComparisonOperatorFromConditionString(string str)
{
str ??= "";
ComparisonOperatorType op = ComparisonOperatorType.Equals;
string conditionStr = str;
if (str.IndexOf(' ') is var i and >= 0)
{
op = GetComparisonOperatorType(str[..i]);
if (op != ComparisonOperatorType.None) { conditionStr = str[(i + 1)..]; }
else { op = ComparisonOperatorType.Equals; }
}
return (op, conditionStr);
}
public static ComparisonOperatorType GetComparisonOperatorType(string op)
{
//thanks xml for not letting me use < or > in attributes :(
switch (op.ToLowerInvariant())
{
case "e":
case "eq":
case "equals":
return ComparisonOperatorType.Equals;
case "ne":
case "neq":
case "notequals":
case "!":
case "!e":
case "!eq":
case "!equals":
return ComparisonOperatorType.NotEquals;
case "gt":
case "greaterthan":
return ComparisonOperatorType.GreaterThan;
case "lt":
case "lessthan":
return ComparisonOperatorType.LessThan;
case "gte":
case "gteq":
case "greaterthanequals":
return ComparisonOperatorType.GreaterThanEquals;
case "lte":
case "lteq":
case "lessthanequals":
return ComparisonOperatorType.LessThanEquals;
default:
return ComparisonOperatorType.None;
}
}
private bool ComparisonOperatorIsNotEquals => ComparisonOperator == ComparisonOperatorType.NotEquals;
public bool Matches(ISerializableEntity? target)
{
return TargetContainedItem
? MatchesContained(target)
: MatchesDirect(target);
}
private bool MatchesContained(ISerializableEntity? target)
{
var containedItems = target switch
{
Item item
=> item.ContainedItems,
ItemComponent ic
=> ic.Item.ContainedItems,
Character {Inventory: { } characterInventory}
=> characterInventory.AllItems,
_
=> Enumerable.Empty<Item>()
};
foreach (var containedItem in containedItems)
{
if (MatchesDirect(containedItem)) { return true; }
}
return false;
}
private bool MatchesDirect(ISerializableEntity? target)
{
Character? targetChar = target as Character;
if (target is Limb limb) { targetChar = limb.character; }
switch (Type)
{
case ConditionType.PropertyValueOrAffliction:
// If an AfflictionPrefab with identifier AttributeName exists,
// check for an affliction affecting the target
if (AfflictionPrefab.Prefabs.ContainsKey(AttributeName))
{
if (targetChar is { CharacterHealth: { } health })
{
var affliction = health.GetAffliction(AttributeName);
float afflictionStrength = affliction?.Strength ?? 0f;
return NumberMatchesRequirement(afflictionStrength);
}
}
// Otherwise try checking for a property belonging to the target
else if (target?.SerializableProperties != null
&& target.SerializableProperties.TryGetValue(AttributeName, out var property))
{
return PropertyMatchesRequirement(target, property);
}
else if (targetChar?.SerializableProperties != null
&& targetChar.SerializableProperties.TryGetValue(AttributeName, out var characterProperty))
{
return PropertyMatchesRequirement(targetChar, characterProperty);
}
else if (targetChar?.AnimController?.SerializableProperties is { } animControllerProperties
&& animControllerProperties.TryGetValue(AttributeName, out var animControllerProperty))
{
return PropertyMatchesRequirement(targetChar.AnimController, animControllerProperty);
}
return ComparisonOperatorIsNotEquals;
case ConditionType.SkillRequirement:
if (targetChar != null)
{
float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier());
return NumberMatchesRequirement(skillLevel);
}
return ComparisonOperatorIsNotEquals;
case ConditionType.HasTag:
if (targetChar != null)
{
return CheckMatchingTags(targetChar.Params.HasTag);
}
if (target is Item item)
{
return CheckMatchingTags(item.HasTag);
}
return ComparisonOperatorIsNotEquals;
case ConditionType.HasStatusTag:
if (target == null) { return ComparisonOperatorIsNotEquals; }
int numTagsFound = 0;
foreach (var tag in AttributeValueAsTags)
{
bool tagFound = false;
foreach (var durationEffect in StatusEffect.DurationList)
{
if (!durationEffect.Targets.Contains(target)) { continue; }
if (durationEffect.Parent.HasTag(tag))
{
tagFound = true;
break;
}
}
if (!tagFound)
{
foreach (var delayedEffect in DelayedEffect.DelayList)
{
if (!delayedEffect.Targets.Contains(target)) { continue; }
if (delayedEffect.Parent.HasTag(tag))
{
tagFound = true;
break;
}
}
}
if (tagFound)
{
numTagsFound++;
}
}
return ComparisonOperatorIsNotEquals
? numTagsFound < AttributeValueAsTags.Length // true when some tag wasn't found
: numTagsFound >= AttributeValueAsTags.Length; // true when all the tags are found
case ConditionType.LevelDifficulty:
if (Level.Loaded is { } level)
{
return NumberMatchesRequirement(level.Difficulty);
}
return false;
case ConditionType.WorldHostility:
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
return Compare(campaign.Settings.WorldHostility, cachedHostilityValue, ComparisonOperator);
}
return false;
default:
bool equals = CheckOnlyEquality(target);
return ComparisonOperatorIsNotEquals
? !equals
: equals;
}
}
private bool CheckOnlyEquality(ISerializableEntity? target)
{
switch (Type)
{
case ConditionType.Name:
if (target == null) { return false; }
return target.Name == AttributeValue;
case ConditionType.HasSpecifierTag:
{
if (target is not Character {Info: { } characterInfo})
{
return false;
}
return AttributeValueAsTags.All(characterInfo.Head.Preset.TagSet.Contains);
}
case ConditionType.SpeciesName:
{
if (target is Character targetCharacter)
{
return targetCharacter.SpeciesName == AttributeValue;
}
else if (target is Limb targetLimb)
{
return targetLimb.character.SpeciesName == AttributeValue;
}
return false;
}
case ConditionType.SpeciesGroup:
{
if (target is Character targetCharacter)
{
return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Params.Group);
}
else if (target is Limb targetLimb)
{
return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetLimb.character.Params.Group);
}
return false;
}
case ConditionType.EntityType:
return AttributeValue.ToLowerInvariant() switch
{
"character"
=> target is Character,
"limb"
=> target is Limb,
"item"
=> target is Item,
"structure"
=> target is Structure,
"null"
=> target == null,
_
=> false
};
case ConditionType.LimbType:
{
return target is Limb limb
&& Enum.TryParse(AttributeValue, ignoreCase: true, out LimbType attributeLimbType)
&& attributeLimbType == limb.type;
}
}
return false;
}
private bool SufficientTagMatches(int matches)
{
return ComparisonOperatorIsNotEquals
? matches <= 0
: matches >= AttributeValueAsTags.Length;
}
private bool CheckMatchingTags(Func<Identifier, bool> predicate)
{
int matches = 0;
foreach (Identifier tag in AttributeValueAsTags)
{
if (predicate(tag)) { matches++; }
}
return SufficientTagMatches(matches);
}
public bool TargetTagMatchesTagCondition(Identifier targetTag)
{
if (targetTag.IsEmpty || Type != ConditionType.HasTag) { return false; }
return CheckMatchingTags(targetTag.Equals);
}
private bool NumberMatchesRequirement(float testedValue)
{
if (!FloatValue.HasValue) { return ComparisonOperatorIsNotEquals; }
float value = FloatValue.Value;
return CompareFloat(testedValue, value, ComparisonOperator);
}
private bool PropertyMatchesRequirement(ISerializableEntity target, SerializableProperty property)
{
Type type = property.PropertyType;
if (type == typeof(float) || type == typeof(int))
{
float floatValue = property.GetFloatValue(target);
return NumberMatchesRequirement(floatValue);
}
switch (ComparisonOperator)
{
case ComparisonOperatorType.Equals:
case ComparisonOperatorType.NotEquals:
bool equals;
if (type == typeof(bool))
{
bool attributeValueBool = AttributeValue.IsTrueString();
equals = property.GetBoolValue(target) == attributeValueBool;
}
else
{
var value = property.GetValue(target);
equals = AreValuesEquivalent(value, AttributeValue);
}
return ComparisonOperatorIsNotEquals
? !equals
: equals;
default:
DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! "
+ "Make sure the type of the value set in the config files matches the type of the property.");
return false;
}
static bool AreValuesEquivalent(object? value, string desiredValue)
{
if (value == null)
{
return desiredValue.Equals("null", StringComparison.OrdinalIgnoreCase);
}
else
{
return (value.ToString() ?? "").Equals(desiredValue);
}
}
}
public static bool CompareFloat(float val1, float val2, ComparisonOperatorType op)
{
switch (op)
{
case ComparisonOperatorType.Equals:
return MathUtils.NearlyEqual(val1, val2);
case ComparisonOperatorType.GreaterThan:
return val1 > val2;
case ComparisonOperatorType.GreaterThanEquals:
return val1 >= val2;
case ComparisonOperatorType.LessThan:
return val1 < val2;
case ComparisonOperatorType.LessThanEquals:
return val1 <= val2;
case ComparisonOperatorType.NotEquals:
return !MathUtils.NearlyEqual(val1, val2);
default:
return false;
}
}
public static bool Compare<T>(T leftValue, T rightValue, ComparisonOperatorType comparisonOperator) where T : IComparable
{
return comparisonOperator switch
{
ComparisonOperatorType.NotEquals => leftValue.CompareTo(rightValue) != 0,
ComparisonOperatorType.GreaterThan => leftValue.CompareTo(rightValue) > 0,
ComparisonOperatorType.LessThan => leftValue.CompareTo(rightValue) < 0,
ComparisonOperatorType.GreaterThanEquals => leftValue.CompareTo(rightValue) >= 0,
ComparisonOperatorType.LessThanEquals => leftValue.CompareTo(rightValue) <= 0,
_ => leftValue.CompareTo(rightValue) == 0,
};
}
/// <summary>
/// Seeks for child elements of name "conditional" and bundles them with an attribute of name "comparison".
/// </summary>
public static LogicalComparison? LoadConditionals(ContentXElement element, LogicalOperatorType defaultOperatorType = LogicalOperatorType.And)
{
var conditionalElements = element.GetChildElements("conditional");
if (conditionalElements.None()) { return default; }
List<PropertyConditional> conditionals = new();
foreach (ContentXElement subElement in conditionalElements)
{
conditionals.AddRange(FromXElement(subElement));
}
var logicalOperator = element.GetAttributeEnum("comparison", defaultOperatorType);
return new LogicalComparison(conditionals, logicalOperator);
}
public static bool CheckConditionals(ISerializableEntity conditionalTarget, IEnumerable<PropertyConditional> conditionals, LogicalOperatorType logicalOperator)
{
if (conditionals == null) { return true; }
if (conditionals.None()) { return true; }
switch (logicalOperator)
{
case LogicalOperatorType.And:
foreach (var conditional in conditionals)
{
if (!conditional.Matches(conditionalTarget))
{
// Some conditional didn't match.
return false;
}
}
// All conditionals matched.
return true;
case LogicalOperatorType.Or:
foreach (var conditional in conditionals)
{
if (conditional.Matches(conditionalTarget))
{
// Some conditional matched.
return true;
}
}
// None of the conditionals matched.
return false;
default:
throw new NotSupportedException();
}
}
/// <summary>
/// Bundles up a bunch of conditionals with a logical operator.
/// </summary>
public class LogicalComparison
{
public readonly ImmutableArray<PropertyConditional> Conditionals;
public readonly LogicalOperatorType LogicalOperator;
public LogicalComparison(IEnumerable<PropertyConditional> conditionals, LogicalOperatorType logicalOperator)
{
Conditionals = conditionals.ToImmutableArray();
LogicalOperator = logicalOperator;
}
}
}
}