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;
9 
10 namespace Barotrauma
11 {
18  sealed class PropertyConditional
19  {
20  // TODO: Make this testable and add tests
21 
25  public enum ConditionType
26  {
41  PropertyValueOrAffliction,
42 
53  SkillRequirement,
54 
58  Name,
59 
63  SpeciesName,
64 
68  SpeciesGroup,
69 
75  HasTag,
76 
82  HasStatusTag,
83 
90  HasSpecifierTag,
91 
97  EntityType,
98 
102  LimbType,
103 
107  WorldHostility
108  }
109 
111  {
112  And,
113  Or
114  }
115 
121  {
122  None,
123 
129  Equals,
130 
134  NotEquals,
135 
142  LessThan,
143 
150  LessThanEquals,
151 
158  GreaterThan,
159 
166  GreaterThanEquals
167  }
168 
169  public readonly ConditionType Type;
171  public readonly Identifier AttributeName;
172  public readonly string AttributeValue;
173  public readonly ImmutableArray<Identifier> AttributeValueAsTags;
174  public readonly float? FloatValue;
175 
176  private readonly WorldHostilityOption cachedHostilityValue;
177 
182  public readonly string TargetItemComponent;
183 
188  public readonly bool TargetSelf;
189 
193  public readonly bool TargetContainer;
194 
199  public readonly bool TargetGrandParent;
200 
204  public readonly bool TargetContainedItem;
205 
206  public static IEnumerable<PropertyConditional> FromXElement(ContentXElement element, Predicate<XAttribute>? predicate = null)
207  {
208  var targetItemComponent = element.GetAttributeString(nameof(TargetItemComponent), "");
209  var targetContainer = element.GetAttributeBool(nameof(TargetContainer), false);
210  var targetSelf = element.GetAttributeBool(nameof(TargetSelf), false);
211  var targetGrandParent = element.GetAttributeBool(nameof(TargetGrandParent), false);
212  var targetContainedItem = element.GetAttributeBool(nameof(TargetContainedItem), false);
213 
214  ConditionType? overrideConditionType = null;
215  if (element.GetAttributeBool(nameof(ConditionType.SkillRequirement), false))
216  {
217  overrideConditionType = ConditionType.SkillRequirement;
218  }
219 
220  foreach (var attribute in element.Attributes())
221  {
222  if (!IsValid(attribute)) { continue; }
223  if (predicate != null && !predicate(attribute)) { continue; }
224 
225  var (comparisonOperator, attributeValueString) = ExtractComparisonOperatorFromConditionString(attribute.Value);
226  if (string.IsNullOrWhiteSpace(attributeValueString))
227  {
228  DebugConsole.ThrowError($"Conditional attribute value is empty: {element}", contentPackage: element.ContentPackage);
229  continue;
230  }
231 
232  var conditionType = overrideConditionType ??
233  (Enum.TryParse(attribute.Name.LocalName, ignoreCase: true, out ConditionType type)
234  ? type
235  : ConditionType.PropertyValueOrAffliction);
236 
237  yield return new PropertyConditional(
238  attributeName: attribute.NameAsIdentifier(),
239  comparisonOperator: comparisonOperator,
240  attributeValue: attributeValueString,
241  targetItemComponent: targetItemComponent,
242  targetSelf: targetSelf,
243  targetContainer: targetContainer,
244  targetGrandParent: targetGrandParent,
245  targetContainedItem: targetContainedItem,
246  conditionType: conditionType);
247  }
248  }
249 
250  private static bool IsValid(XAttribute attribute)
251  {
252  switch (attribute.Name.ToString().ToLowerInvariant())
253  {
254  case "targetitemcomponent":
255  case "targetself":
256  case "targetcontainer":
257  case "targetgrandparent":
258  case "targetcontaineditem":
259  case "skillrequirement":
260  case "targetslot":
261  return false;
262  default:
263  return true;
264  }
265  }
266 
267  private PropertyConditional(
268  Identifier attributeName,
269  ComparisonOperatorType comparisonOperator,
270  string attributeValue,
271  string targetItemComponent,
272  bool targetSelf,
273  bool targetContainer,
274  bool targetGrandParent,
275  bool targetContainedItem,
276  ConditionType conditionType)
277  {
278  AttributeName = attributeName;
279 
280  TargetItemComponent = targetItemComponent;
281  TargetSelf = targetSelf;
282  TargetContainer = targetContainer;
283  TargetGrandParent = targetGrandParent;
284  TargetContainedItem = targetContainedItem;
285 
286  Type = conditionType;
287 
288  ComparisonOperator = comparisonOperator;
289  AttributeValue = attributeValue;
291  .Select(s => s.ToIdentifier())
292  .ToImmutableArray();
293  if (float.TryParse(AttributeValue, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
294  {
295  FloatValue = value;
296  }
297 
298  if (Type == ConditionType.WorldHostility && Enum.TryParse(AttributeValue, ignoreCase: true, out WorldHostilityOption hostilityValue))
299  {
300  cachedHostilityValue = hostilityValue;
301  }
302  }
303 
304  public static (ComparisonOperatorType ComparisonOperator, string ConditionStr) ExtractComparisonOperatorFromConditionString(string str)
305  {
306  str ??= "";
307 
309  string conditionStr = str;
310  if (str.IndexOf(' ') is var i and >= 0)
311  {
312  op = GetComparisonOperatorType(str[..i]);
313  if (op != ComparisonOperatorType.None) { conditionStr = str[(i + 1)..]; }
314  else { op = ComparisonOperatorType.Equals; }
315  }
316  return (op, conditionStr);
317  }
318 
320  {
321  //thanks xml for not letting me use < or > in attributes :(
322  switch (op.ToLowerInvariant())
323  {
324  case "e":
325  case "eq":
326  case "equals":
327  return ComparisonOperatorType.Equals;
328  case "ne":
329  case "neq":
330  case "notequals":
331  case "!":
332  case "!e":
333  case "!eq":
334  case "!equals":
335  return ComparisonOperatorType.NotEquals;
336  case "gt":
337  case "greaterthan":
338  return ComparisonOperatorType.GreaterThan;
339  case "lt":
340  case "lessthan":
341  return ComparisonOperatorType.LessThan;
342  case "gte":
343  case "gteq":
344  case "greaterthanequals":
345  return ComparisonOperatorType.GreaterThanEquals;
346  case "lte":
347  case "lteq":
348  case "lessthanequals":
349  return ComparisonOperatorType.LessThanEquals;
350  default:
351  return ComparisonOperatorType.None;
352  }
353  }
354 
355  private bool ComparisonOperatorIsNotEquals => ComparisonOperator == ComparisonOperatorType.NotEquals;
356 
357  public bool Matches(ISerializableEntity? target)
358  {
359  return TargetContainedItem
360  ? MatchesContained(target)
361  : MatchesDirect(target);
362  }
363 
364  private bool MatchesContained(ISerializableEntity? target)
365  {
366  var containedItems = target switch
367  {
368  Item item
369  => item.ContainedItems,
370  ItemComponent ic
371  => ic.Item.ContainedItems,
372  Character {Inventory: { } characterInventory}
373  => characterInventory.AllItems,
374  _
375  => Enumerable.Empty<Item>()
376  };
377  foreach (var containedItem in containedItems)
378  {
379  if (MatchesDirect(containedItem)) { return true; }
380  }
381  return false;
382  }
383 
384  private bool MatchesDirect(ISerializableEntity? target)
385  {
386  Character? targetChar = target as Character;
387  if (target is Limb limb) { targetChar = limb.character; }
388  switch (Type)
389  {
390  case ConditionType.PropertyValueOrAffliction:
391  // If an AfflictionPrefab with identifier AttributeName exists,
392  // check for an affliction affecting the target
393  if (AfflictionPrefab.Prefabs.ContainsKey(AttributeName))
394  {
395  if (targetChar is { CharacterHealth: { } health })
396  {
397  var affliction = health.GetAffliction(AttributeName);
398  float afflictionStrength = affliction?.Strength ?? 0f;
399 
400  return NumberMatchesRequirement(afflictionStrength);
401  }
402  }
403  // Otherwise try checking for a property belonging to the target
404  else if (target?.SerializableProperties != null
405  && target.SerializableProperties.TryGetValue(AttributeName, out var property))
406  {
407  return PropertyMatchesRequirement(target, property);
408  }
409  else if (targetChar?.SerializableProperties != null
410  && targetChar.SerializableProperties.TryGetValue(AttributeName, out var characterProperty))
411  {
412  return PropertyMatchesRequirement(targetChar, characterProperty);
413  }
414  return ComparisonOperatorIsNotEquals;
415  case ConditionType.SkillRequirement:
416  if (targetChar != null)
417  {
418  float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier());
419 
420  return NumberMatchesRequirement(skillLevel);
421  }
422  return ComparisonOperatorIsNotEquals;
423  case ConditionType.HasTag:
424  return ItemMatchesTagCondition(target);
425  case ConditionType.HasStatusTag:
426  if (target == null) { return ComparisonOperatorIsNotEquals; }
427 
428  int numTagsFound = 0;
429  foreach (var tag in AttributeValueAsTags)
430  {
431  bool tagFound = false;
432  foreach (var durationEffect in StatusEffect.DurationList)
433  {
434  if (!durationEffect.Targets.Contains(target)) { continue; }
435  if (durationEffect.Parent.HasTag(tag))
436  {
437  tagFound = true;
438  break;
439  }
440  }
441  if (!tagFound)
442  {
443  foreach (var delayedEffect in DelayedEffect.DelayList)
444  {
445  if (!delayedEffect.Targets.Contains(target)) { continue; }
446  if (delayedEffect.Parent.HasTag(tag))
447  {
448  tagFound = true;
449  break;
450  }
451  }
452  }
453  if (tagFound)
454  {
455  numTagsFound++;
456  }
457  }
458  return ComparisonOperatorIsNotEquals
459  ? numTagsFound < AttributeValueAsTags.Length // true when some tag wasn't found
460  : numTagsFound >= AttributeValueAsTags.Length; // true when all the tags are found
461  case ConditionType.WorldHostility:
462  {
463  if (GameMain.GameSession?.Campaign is CampaignMode campaign)
464  {
465  return Compare(campaign.Settings.WorldHostility, cachedHostilityValue, ComparisonOperator);
466  }
467  return false;
468  }
469  default:
470  bool equals = CheckOnlyEquality(target);
471  return ComparisonOperatorIsNotEquals
472  ? !equals
473  : equals;
474  }
475  }
476 
477  private bool CheckOnlyEquality(ISerializableEntity? target)
478  {
479  switch (Type)
480  {
481  case ConditionType.Name:
482  if (target == null) { return false; }
483 
484  return target.Name == AttributeValue;
485  case ConditionType.HasSpecifierTag:
486  {
487  if (target is not Character {Info: { } characterInfo})
488  {
489  return false;
490  }
491 
492  return AttributeValueAsTags.All(characterInfo.Head.Preset.TagSet.Contains);
493  }
494  case ConditionType.SpeciesName:
495  {
496  if (target is Character targetCharacter)
497  {
498  return targetCharacter.SpeciesName == AttributeValue;
499  }
500  else if (target is Limb targetLimb)
501  {
502  return targetLimb.character.SpeciesName == AttributeValue;
503  }
504  return false;
505  }
506  case ConditionType.SpeciesGroup:
507  {
508  if (target is Character targetCharacter)
509  {
510  return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Params.Group);
511  }
512  else if (target is Limb targetLimb)
513  {
514  return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetLimb.character.Params.Group);
515  }
516  return false;
517  }
518  case ConditionType.EntityType:
519  return AttributeValue.ToLowerInvariant() switch
520  {
521  "character"
522  => target is Character,
523  "limb"
524  => target is Limb,
525  "item"
526  => target is Item,
527  "structure"
528  => target is Structure,
529  "null"
530  => target == null,
531  _
532  => false
533  };
534  case ConditionType.LimbType:
535  {
536  return target is Limb limb
537  && Enum.TryParse(AttributeValue, ignoreCase: true, out LimbType attributeLimbType)
538  && attributeLimbType == limb.type;
539  }
540  }
541  return false;
542  }
543 
544  private bool SufficientTagMatches(int matches)
545  {
546  return ComparisonOperatorIsNotEquals
547  ? matches <= 0
548  : matches >= AttributeValueAsTags.Length;
549  }
550 
551  private bool ItemMatchesTagCondition(ISerializableEntity? target)
552  {
553  if (target is not Item item) { return ComparisonOperatorIsNotEquals; }
554 
555  int matches = 0;
556  foreach (var tag in AttributeValueAsTags)
557  {
558  if (item.HasTag(tag)) { matches++; }
559  }
560  return SufficientTagMatches(matches);
561  }
562 
563  public bool TargetTagMatchesTagCondition(Identifier targetTag)
564  {
565  if (targetTag.IsEmpty || Type != ConditionType.HasTag) { return false; }
566 
567  int matches = 0;
568  foreach (var tag in AttributeValueAsTags)
569  {
570  if (targetTag == tag) { matches++; }
571  }
572  return SufficientTagMatches(matches);
573  }
574 
575  private bool NumberMatchesRequirement(float testedValue)
576  {
577  if (!FloatValue.HasValue) { return ComparisonOperatorIsNotEquals; }
578  float value = FloatValue.Value;
579  return CompareFloat(testedValue, value, ComparisonOperator);
580  }
581 
582  private bool PropertyMatchesRequirement(ISerializableEntity target, SerializableProperty property)
583  {
584  Type type = property.PropertyType;
585 
586  if (type == typeof(float) || type == typeof(int))
587  {
588  float floatValue = property.GetFloatValue(target);
589  return NumberMatchesRequirement(floatValue);
590  }
591 
592  switch (ComparisonOperator)
593  {
594  case ComparisonOperatorType.Equals:
595  case ComparisonOperatorType.NotEquals:
596  bool equals;
597  if (type == typeof(bool))
598  {
599  bool attributeValueBool = AttributeValue.IsTrueString();
600  equals = property.GetBoolValue(target) == attributeValueBool;
601  }
602  else
603  {
604  var value = property.GetValue(target);
605  equals = AreValuesEquivalent(value, AttributeValue);
606  }
607 
608  return ComparisonOperatorIsNotEquals
609  ? !equals
610  : equals;
611  default:
612  DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! "
613  + "Make sure the type of the value set in the config files matches the type of the property.");
614  return false;
615  }
616 
617  static bool AreValuesEquivalent(object? value, string desiredValue)
618  {
619  if (value == null)
620  {
621  return desiredValue.Equals("null", StringComparison.OrdinalIgnoreCase);
622  }
623  else
624  {
625  return (value.ToString() ?? "").Equals(desiredValue);
626  }
627  }
628  }
629 
630  public static bool CompareFloat(float val1, float val2, ComparisonOperatorType op)
631  {
632  switch (op)
633  {
634  case ComparisonOperatorType.Equals:
635  return MathUtils.NearlyEqual(val1, val2);
636  case ComparisonOperatorType.GreaterThan:
637  return val1 > val2;
638  case ComparisonOperatorType.GreaterThanEquals:
639  return val1 >= val2;
640  case ComparisonOperatorType.LessThan:
641  return val1 < val2;
642  case ComparisonOperatorType.LessThanEquals:
643  return val1 <= val2;
644  case ComparisonOperatorType.NotEquals:
645  return !MathUtils.NearlyEqual(val1, val2);
646  default:
647  return false;
648  }
649  }
650 
651  public static bool Compare<T>(T leftValue, T rightValue, ComparisonOperatorType comparisonOperator) where T : IComparable
652  {
653  return comparisonOperator switch
654  {
655  ComparisonOperatorType.NotEquals => leftValue.CompareTo(rightValue) != 0,
656  ComparisonOperatorType.GreaterThan => leftValue.CompareTo(rightValue) > 0,
657  ComparisonOperatorType.LessThan => leftValue.CompareTo(rightValue) < 0,
658  ComparisonOperatorType.GreaterThanEquals => leftValue.CompareTo(rightValue) >= 0,
659  ComparisonOperatorType.LessThanEquals => leftValue.CompareTo(rightValue) <= 0,
660  _ => leftValue.CompareTo(rightValue) == 0,
661  };
662  }
663  }
664 }
string? GetAttributeString(string key, string? def)
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
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)
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)
ComparisonOperatorType
There are several ways to compare properties to values. The comparison operator to use can be specifi...
readonly ImmutableArray< Identifier > AttributeValueAsTags
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:707