Client LuaCsForBarotrauma
PropertyConditional.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Collections.Immutable;
5 using System.Globalization;
6 using System.Linq;
7 using System.Xml.Linq;
10 
11 namespace Barotrauma
12 {
19  sealed class PropertyConditional
20  {
21  // TODO: Make this testable and add tests
22 
26  public enum ConditionType
27  {
42  PropertyValueOrAffliction,
43 
54  SkillRequirement,
55 
59  Name,
60 
64  SpeciesName,
65 
69  SpeciesGroup,
70 
76  HasTag,
77 
83  HasStatusTag,
84 
91  HasSpecifierTag,
92 
98  EntityType,
99 
103  LimbType,
104 
108  WorldHostility,
109 
113  LevelDifficulty
114  }
115 
117  {
118  And,
119  Or
120  }
121 
127  {
128  None,
129 
135  Equals,
136 
140  NotEquals,
141 
148  LessThan,
149 
156  LessThanEquals,
157 
164  GreaterThan,
165 
172  GreaterThanEquals
173  }
174 
175  public readonly ConditionType Type;
177  public readonly Identifier AttributeName;
178  public readonly string AttributeValue;
179  public readonly ImmutableArray<Identifier> AttributeValueAsTags;
180  public readonly float? FloatValue;
181 
182  private readonly WorldHostilityOption cachedHostilityValue;
183 
188  public readonly string TargetItemComponent;
189 
194 
199  public readonly bool TargetSelf;
200 
204  public readonly bool TargetContainer;
205 
210  public readonly bool TargetGrandParent;
211 
215  public readonly bool TargetContainedItem;
216 
217  public static IEnumerable<PropertyConditional> FromXElement(ContentXElement element, Predicate<XAttribute>? predicate = null)
218  {
219  string targetItemComponent = element.GetAttributeString(nameof(TargetItemComponent), string.Empty);
220  bool targetContainer = element.GetAttributeBool(nameof(TargetContainer), false);
221  bool targetSelf = element.GetAttributeBool(nameof(TargetSelf), false);
222  bool targetGrandParent = element.GetAttributeBool(nameof(TargetGrandParent), false);
223  bool targetContainedItem = element.GetAttributeBool(nameof(TargetContainedItem), false);
224 
225  LogicalOperatorType itemComponentComparison = element.GetAttributeEnum(nameof(ItemComponentComparison), LogicalOperatorType.Or);
226 
227  ConditionType? overrideConditionType = null;
228  if (element.GetAttributeBool(nameof(ConditionType.SkillRequirement), false))
229  {
230  overrideConditionType = ConditionType.SkillRequirement;
231  }
232 
233  foreach (var attribute in element.Attributes())
234  {
235  if (!IsValid(attribute)) { continue; }
236  if (predicate != null && !predicate(attribute)) { continue; }
237 
238  (ComparisonOperatorType comparisonOperator, string attributeValueString) = ExtractComparisonOperatorFromConditionString(attribute.Value);
239  if (string.IsNullOrWhiteSpace(attributeValueString))
240  {
241  DebugConsole.ThrowError($"Conditional attribute value is empty: {element}", contentPackage: element.ContentPackage);
242  continue;
243  }
244 
245  ConditionType conditionType = overrideConditionType ??
246  (Enum.TryParse(attribute.Name.LocalName, ignoreCase: true, out ConditionType type) ? type : ConditionType.PropertyValueOrAffliction);
247 
248  yield return new PropertyConditional(
249  attributeName: attribute.NameAsIdentifier(),
250  comparisonOperator: comparisonOperator,
251  attributeValue: attributeValueString,
252  targetItemComponent: targetItemComponent,
253  itemComponentComparison: itemComponentComparison,
254  targetSelf: targetSelf,
255  targetContainer: targetContainer,
256  targetGrandParent: targetGrandParent,
257  targetContainedItem: targetContainedItem,
258  conditionType: conditionType);
259  }
260  }
261 
262  private static bool IsValid(XAttribute attribute)
263  {
264  switch (attribute.Name.ToString().ToLowerInvariant())
265  {
266  case "targetitemcomponent":
267  case "targetself":
268  case "targetcontainer":
269  case "targetgrandparent":
270  case "targetcontaineditem":
271  case "skillrequirement":
272  case "targetslot":
273  return false;
274  default:
275  return true;
276  }
277  }
278 
279  private PropertyConditional(
280  Identifier attributeName,
281  ComparisonOperatorType comparisonOperator,
282  string attributeValue,
283  string targetItemComponent,
284  LogicalOperatorType itemComponentComparison,
285  bool targetSelf,
286  bool targetContainer,
287  bool targetGrandParent,
288  bool targetContainedItem,
289  ConditionType conditionType)
290  {
291  AttributeName = attributeName;
292 
293  TargetItemComponent = targetItemComponent;
294  ItemComponentComparison = itemComponentComparison;
295  TargetSelf = targetSelf;
296  TargetContainer = targetContainer;
297  TargetGrandParent = targetGrandParent;
298  TargetContainedItem = targetContainedItem;
299 
300  Type = conditionType;
301 
302  ComparisonOperator = comparisonOperator;
303  AttributeValue = attributeValue;
305  .Select(s => s.ToIdentifier())
306  .ToImmutableArray();
307  if (float.TryParse(AttributeValue, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
308  {
309  FloatValue = value;
310  }
311 
312  if (Type == ConditionType.WorldHostility && Enum.TryParse(AttributeValue, ignoreCase: true, out WorldHostilityOption hostilityValue))
313  {
314  cachedHostilityValue = hostilityValue;
315  }
316  }
317 
318  public static (ComparisonOperatorType ComparisonOperator, string ConditionStr) ExtractComparisonOperatorFromConditionString(string str)
319  {
320  str ??= "";
321 
323  string conditionStr = str;
324  if (str.IndexOf(' ') is var i and >= 0)
325  {
326  op = GetComparisonOperatorType(str[..i]);
327  if (op != ComparisonOperatorType.None) { conditionStr = str[(i + 1)..]; }
328  else { op = ComparisonOperatorType.Equals; }
329  }
330  return (op, conditionStr);
331  }
332 
334  {
335  //thanks xml for not letting me use < or > in attributes :(
336  switch (op.ToLowerInvariant())
337  {
338  case "e":
339  case "eq":
340  case "equals":
341  return ComparisonOperatorType.Equals;
342  case "ne":
343  case "neq":
344  case "notequals":
345  case "!":
346  case "!e":
347  case "!eq":
348  case "!equals":
349  return ComparisonOperatorType.NotEquals;
350  case "gt":
351  case "greaterthan":
352  return ComparisonOperatorType.GreaterThan;
353  case "lt":
354  case "lessthan":
355  return ComparisonOperatorType.LessThan;
356  case "gte":
357  case "gteq":
358  case "greaterthanequals":
359  return ComparisonOperatorType.GreaterThanEquals;
360  case "lte":
361  case "lteq":
362  case "lessthanequals":
363  return ComparisonOperatorType.LessThanEquals;
364  default:
365  return ComparisonOperatorType.None;
366  }
367  }
368 
369  private bool ComparisonOperatorIsNotEquals => ComparisonOperator == ComparisonOperatorType.NotEquals;
370 
371  public bool Matches(ISerializableEntity? target)
372  {
373  return TargetContainedItem
374  ? MatchesContained(target)
375  : MatchesDirect(target);
376  }
377 
378  private bool MatchesContained(ISerializableEntity? target)
379  {
380  var containedItems = target switch
381  {
382  Item item
383  => item.ContainedItems,
384  ItemComponent ic
385  => ic.Item.ContainedItems,
386  Character {Inventory: { } characterInventory}
387  => characterInventory.AllItems,
388  _
389  => Enumerable.Empty<Item>()
390  };
391  foreach (var containedItem in containedItems)
392  {
393  if (MatchesDirect(containedItem)) { return true; }
394  }
395  return false;
396  }
397 
398  private bool MatchesDirect(ISerializableEntity? target)
399  {
400  Character? targetChar = target as Character;
401  if (target is Limb limb) { targetChar = limb.character; }
402  switch (Type)
403  {
404  case ConditionType.PropertyValueOrAffliction:
405  // If an AfflictionPrefab with identifier AttributeName exists,
406  // check for an affliction affecting the target
407  if (AfflictionPrefab.Prefabs.ContainsKey(AttributeName))
408  {
409  if (targetChar is { CharacterHealth: { } health })
410  {
411  var affliction = health.GetAffliction(AttributeName);
412  float afflictionStrength = affliction?.Strength ?? 0f;
413 
414  return NumberMatchesRequirement(afflictionStrength);
415  }
416  }
417  // Otherwise try checking for a property belonging to the target
418  else if (target?.SerializableProperties != null
419  && target.SerializableProperties.TryGetValue(AttributeName, out var property))
420  {
421  return PropertyMatchesRequirement(target, property);
422  }
423  else if (targetChar?.SerializableProperties != null
424  && targetChar.SerializableProperties.TryGetValue(AttributeName, out var characterProperty))
425  {
426  return PropertyMatchesRequirement(targetChar, characterProperty);
427  }
428  return ComparisonOperatorIsNotEquals;
429  case ConditionType.SkillRequirement:
430  if (targetChar != null)
431  {
432  float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier());
433 
434  return NumberMatchesRequirement(skillLevel);
435  }
436  return ComparisonOperatorIsNotEquals;
437  case ConditionType.HasTag:
438  if (targetChar != null)
439  {
440  return CheckMatchingTags(targetChar.Params.HasTag);
441  }
442  if (target is Item item)
443  {
444  return CheckMatchingTags(item.HasTag);
445  }
446  return ComparisonOperatorIsNotEquals;
447  case ConditionType.HasStatusTag:
448  if (target == null) { return ComparisonOperatorIsNotEquals; }
449 
450  int numTagsFound = 0;
451  foreach (var tag in AttributeValueAsTags)
452  {
453  bool tagFound = false;
454  foreach (var durationEffect in StatusEffect.DurationList)
455  {
456  if (!durationEffect.Targets.Contains(target)) { continue; }
457  if (durationEffect.Parent.HasTag(tag))
458  {
459  tagFound = true;
460  break;
461  }
462  }
463  if (!tagFound)
464  {
465  foreach (var delayedEffect in DelayedEffect.DelayList)
466  {
467  if (!delayedEffect.Targets.Contains(target)) { continue; }
468  if (delayedEffect.Parent.HasTag(tag))
469  {
470  tagFound = true;
471  break;
472  }
473  }
474  }
475  if (tagFound)
476  {
477  numTagsFound++;
478  }
479  }
480  return ComparisonOperatorIsNotEquals
481  ? numTagsFound < AttributeValueAsTags.Length // true when some tag wasn't found
482  : numTagsFound >= AttributeValueAsTags.Length; // true when all the tags are found
483  case ConditionType.LevelDifficulty:
484  if (Level.Loaded is { } level)
485  {
486  return NumberMatchesRequirement(level.Difficulty);
487  }
488  return false;
489  case ConditionType.WorldHostility:
490  if (GameMain.GameSession?.Campaign is CampaignMode campaign)
491  {
492  return Compare(campaign.Settings.WorldHostility, cachedHostilityValue, ComparisonOperator);
493  }
494  return false;
495  default:
496  bool equals = CheckOnlyEquality(target);
497  return ComparisonOperatorIsNotEquals
498  ? !equals
499  : equals;
500  }
501  }
502 
503  private bool CheckOnlyEquality(ISerializableEntity? target)
504  {
505  switch (Type)
506  {
507  case ConditionType.Name:
508  if (target == null) { return false; }
509 
510  return target.Name == AttributeValue;
511  case ConditionType.HasSpecifierTag:
512  {
513  if (target is not Character {Info: { } characterInfo})
514  {
515  return false;
516  }
517 
518  return AttributeValueAsTags.All(characterInfo.Head.Preset.TagSet.Contains);
519  }
520  case ConditionType.SpeciesName:
521  {
522  if (target is Character targetCharacter)
523  {
524  return targetCharacter.SpeciesName == AttributeValue;
525  }
526  else if (target is Limb targetLimb)
527  {
528  return targetLimb.character.SpeciesName == AttributeValue;
529  }
530  return false;
531  }
532  case ConditionType.SpeciesGroup:
533  {
534  if (target is Character targetCharacter)
535  {
536  return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Params.Group);
537  }
538  else if (target is Limb targetLimb)
539  {
540  return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetLimb.character.Params.Group);
541  }
542  return false;
543  }
544  case ConditionType.EntityType:
545  return AttributeValue.ToLowerInvariant() switch
546  {
547  "character"
548  => target is Character,
549  "limb"
550  => target is Limb,
551  "item"
552  => target is Item,
553  "structure"
554  => target is Structure,
555  "null"
556  => target == null,
557  _
558  => false
559  };
560  case ConditionType.LimbType:
561  {
562  return target is Limb limb
563  && Enum.TryParse(AttributeValue, ignoreCase: true, out LimbType attributeLimbType)
564  && attributeLimbType == limb.type;
565  }
566  }
567  return false;
568  }
569 
570  private bool SufficientTagMatches(int matches)
571  {
572  return ComparisonOperatorIsNotEquals
573  ? matches <= 0
574  : matches >= AttributeValueAsTags.Length;
575  }
576 
577  private bool CheckMatchingTags(Func<Identifier, bool> predicate)
578  {
579  int matches = 0;
580  foreach (Identifier tag in AttributeValueAsTags)
581  {
582  if (predicate(tag)) { matches++; }
583  }
584  return SufficientTagMatches(matches);
585  }
586 
587  public bool TargetTagMatchesTagCondition(Identifier targetTag)
588  {
589  if (targetTag.IsEmpty || Type != ConditionType.HasTag) { return false; }
590  return CheckMatchingTags(targetTag.Equals);
591  }
592 
593  private bool NumberMatchesRequirement(float testedValue)
594  {
595  if (!FloatValue.HasValue) { return ComparisonOperatorIsNotEquals; }
596  float value = FloatValue.Value;
597  return CompareFloat(testedValue, value, ComparisonOperator);
598  }
599 
600  private bool PropertyMatchesRequirement(ISerializableEntity target, SerializableProperty property)
601  {
602  Type type = property.PropertyType;
603 
604  if (type == typeof(float) || type == typeof(int))
605  {
606  float floatValue = property.GetFloatValue(target);
607  return NumberMatchesRequirement(floatValue);
608  }
609 
610  switch (ComparisonOperator)
611  {
612  case ComparisonOperatorType.Equals:
613  case ComparisonOperatorType.NotEquals:
614  bool equals;
615  if (type == typeof(bool))
616  {
617  bool attributeValueBool = AttributeValue.IsTrueString();
618  equals = property.GetBoolValue(target) == attributeValueBool;
619  }
620  else
621  {
622  var value = property.GetValue(target);
623  equals = AreValuesEquivalent(value, AttributeValue);
624  }
625 
626  return ComparisonOperatorIsNotEquals
627  ? !equals
628  : equals;
629  default:
630  DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! "
631  + "Make sure the type of the value set in the config files matches the type of the property.");
632  return false;
633  }
634 
635  static bool AreValuesEquivalent(object? value, string desiredValue)
636  {
637  if (value == null)
638  {
639  return desiredValue.Equals("null", StringComparison.OrdinalIgnoreCase);
640  }
641  else
642  {
643  return (value.ToString() ?? "").Equals(desiredValue);
644  }
645  }
646  }
647 
648  public static bool CompareFloat(float val1, float val2, ComparisonOperatorType op)
649  {
650  switch (op)
651  {
652  case ComparisonOperatorType.Equals:
653  return MathUtils.NearlyEqual(val1, val2);
654  case ComparisonOperatorType.GreaterThan:
655  return val1 > val2;
656  case ComparisonOperatorType.GreaterThanEquals:
657  return val1 >= val2;
658  case ComparisonOperatorType.LessThan:
659  return val1 < val2;
660  case ComparisonOperatorType.LessThanEquals:
661  return val1 <= val2;
662  case ComparisonOperatorType.NotEquals:
663  return !MathUtils.NearlyEqual(val1, val2);
664  default:
665  return false;
666  }
667  }
668 
669  public static bool Compare<T>(T leftValue, T rightValue, ComparisonOperatorType comparisonOperator) where T : IComparable
670  {
671  return comparisonOperator switch
672  {
673  ComparisonOperatorType.NotEquals => leftValue.CompareTo(rightValue) != 0,
674  ComparisonOperatorType.GreaterThan => leftValue.CompareTo(rightValue) > 0,
675  ComparisonOperatorType.LessThan => leftValue.CompareTo(rightValue) < 0,
676  ComparisonOperatorType.GreaterThanEquals => leftValue.CompareTo(rightValue) >= 0,
677  ComparisonOperatorType.LessThanEquals => leftValue.CompareTo(rightValue) <= 0,
678  _ => leftValue.CompareTo(rightValue) == 0,
679  };
680  }
681 
686  {
687  var conditionalElements = element.GetChildElements("conditional");
688  if (conditionalElements.None()) { return default; }
689  List<PropertyConditional> conditionals = new();
690  foreach (ContentXElement subElement in conditionalElements)
691  {
692  conditionals.AddRange(FromXElement(subElement));
693  }
694  var logicalOperator = element.GetAttributeEnum("comparison", defaultOperatorType);
695  return new LogicalComparison(conditionals, logicalOperator);
696  }
697 
698  public static bool CheckConditionals(ISerializableEntity conditionalTarget, IEnumerable<PropertyConditional> conditionals, LogicalOperatorType logicalOperator)
699  {
700  if (conditionals == null) { return true; }
701  if (conditionals.None()) { return true; }
702  switch (logicalOperator)
703  {
704  case LogicalOperatorType.And:
705  foreach (var conditional in conditionals)
706  {
707  if (!conditional.Matches(conditionalTarget))
708  {
709  // Some conditional didn't match.
710  return false;
711  }
712  }
713  // All conditionals matched.
714  return true;
715  case LogicalOperatorType.Or:
716  foreach (var conditional in conditionals)
717  {
718  if (conditional.Matches(conditionalTarget))
719  {
720  // Some conditional matched.
721  return true;
722  }
723  }
724  // None of the conditionals matched.
725  return false;
726  default:
727  throw new NotSupportedException();
728  }
729  }
730 
734  public class LogicalComparison
735  {
736  public readonly ImmutableArray<PropertyConditional> Conditionals;
738 
739  public LogicalComparison(IEnumerable<PropertyConditional> conditionals, LogicalOperatorType logicalOperator)
740  {
741  Conditionals = conditionals.ToImmutableArray();
742  LogicalOperator = logicalOperator;
743  }
744  }
745  }
746 }
string? GetAttributeString(string key, string? def)
IEnumerable< ContentXElement > GetChildElements(string name)
bool GetAttributeBool(string key, bool def)
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
The base class for components holding the different functionalities of the item
Bundles up a bunch of conditionals with a logical operator.
LogicalComparison(IEnumerable< PropertyConditional > conditionals, LogicalOperatorType logicalOperator)
readonly ImmutableArray< PropertyConditional > Conditionals
Conditionals are used by some in-game mechanics to require one or more conditions to be met for those...
readonly ComparisonOperatorType ComparisonOperator
bool Matches(ISerializableEntity? target)
static bool CheckConditionals(ISerializableEntity conditionalTarget, IEnumerable< PropertyConditional > conditionals, LogicalOperatorType logicalOperator)
readonly bool TargetContainer
If set to true, the conditionals defined by this element check against the entity containing the targ...
bool TargetTagMatchesTagCondition(Identifier targetTag)
static ComparisonOperatorType GetComparisonOperatorType(string op)
readonly bool TargetGrandParent
If this and TargetContainer are set to true, the conditionals defined by this element check against t...
readonly string TargetItemComponent
If set to the name of one of the target's ItemComponents, the conditionals defined by this element ch...
static bool Compare< T >(T leftValue, T rightValue, ComparisonOperatorType comparisonOperator)
static ? LogicalComparison LoadConditionals(ContentXElement element, LogicalOperatorType defaultOperatorType=LogicalOperatorType.And)
Seeks for child elements of name "conditional" and bundles them with an attribute of name "comparison...
ComparisonOperatorType
There are several ways to compare properties to values. The comparison operator to use can be specifi...
readonly ImmutableArray< Identifier > AttributeValueAsTags
readonly LogicalOperatorType ItemComponentComparison
When targeting item components, should we require them all to match the conditional or any (default).
readonly bool TargetContainedItem
If set to true, the conditionals defined by this element check against the items contained by the tar...
static bool CompareFloat(float val1, float val2, ComparisonOperatorType op)
readonly bool TargetSelf
If set to true, the conditionals defined by this element check against the attacking character instea...
static IEnumerable< PropertyConditional > FromXElement(ContentXElement element, Predicate< XAttribute >? predicate=null)
static ComparisonOperatorType ComparisonOperator
ConditionType
Category of properties to check against
WorldHostilityOption
Definition: Enums.cs:733
@ Character
Characters only
@ Structure
Structures and hulls, but also items (for backwards support)!