Client LuaCsForBarotrauma
AIObjectiveLoadItem.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Linq;
8 
9 namespace Barotrauma
10 {
12  {
13  public override Identifier Identifier { get; set; } = "load item".ToIdentifier();
14 
15  protected override bool AllowWhileHandcuffed => false;
16 
17  private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; }
18  private Item Container { get; }
19  private ItemContainer ItemContainer { get; }
20  private ImmutableArray<Identifier> TargetContainerTags { get; }
21  private ImmutableHashSet<Identifier> ValidContainableItemIdentifiers { get; }
22  private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>();
23 
24  private int itemIndex;
25  private AIObjectiveDecontainItem decontainObjective;
26  private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
27  private Item targetItem;
28  private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable";
29 
30  public AIObjectiveLoadItem(Item container, ImmutableArray<Identifier> targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, Identifier option, Character character, AIObjectiveManager objectiveManager, float priorityModifier)
31  : base(character, objectiveManager, priorityModifier)
32  {
33  Container = container;
34  ItemContainer = container?.GetComponent<ItemContainer>();
35  if (ItemContainer?.Inventory == null)
36  {
37  Abandon = true;
38  return;
39  }
40  TargetContainerTags = targetTags;
41  TargetItemCondition = targetCondition;
42  if (!option.IsEmpty)
43  {
44  string optionSpecificDialogueIdentifier = $"{abandonGetItemDialogueIdentifier}.{option}";
45  if (TextManager.ContainsTag(optionSpecificDialogueIdentifier))
46  {
47  abandonGetItemDialogueIdentifier = optionSpecificDialogueIdentifier;
48  }
49  }
50  ValidContainableItemIdentifiers = GetValidContainableItemIdentifiers();
51  if (ValidContainableItemIdentifiers.None())
52  {
53 #if DEBUG
54  DebugConsole.LogError($"No valid containable item identifiers found for the Load Item objective targeting {Container}");
55 #endif
56  Abandon = true;
57  return;
58  }
59  }
60 
61  private enum CheckStatus { Unfinished, Finished }
62 
63  private ImmutableHashSet<Identifier> GetValidContainableItemIdentifiers()
64  {
65  if (AllValidContainableItemIdentifiers.TryGetValue(Container.Prefab, out var existingIdentifiers))
66  {
67  return existingIdentifiers;
68  }
69  // Status effects are often used to alter item condition so using the Containable Item Identifiers directly can lead to unwanted results
70  // For example, placing welding fuel tanks inside oxygen tank shelves
71  bool useDefaultContainableItemIdentifiers = true;
72  var potentialContainablePrefabs = MapEntityPrefab.List
73  .Where(mep => mep is ItemPrefab ip && ItemContainer.ContainableItemIdentifiers.Any(i => i == ip.Identifier || ip.Tags.Contains(i)))
74  .Cast<ItemPrefab>();
75  var validContainableItemIdentifiers = new HashSet<Identifier>();
76  foreach (var component in Container.Components)
77  {
78  if (CheckComponent() == CheckStatus.Finished)
79  {
80  break;
81  }
82  CheckStatus CheckComponent()
83  {
84  if (component.statusEffectLists != null)
85  {
86  foreach (var (_, statusEffects) in component.statusEffectLists)
87  {
88  if (CheckStatusEffects(statusEffects) == CheckStatus.Finished)
89  {
90  return CheckStatus.Finished;
91  }
92  }
93  }
94  if (component is ItemContainer itemContainer && itemContainer.ContainableItems != null)
95  {
96  foreach (var item in itemContainer.ContainableItems)
97  {
98  if (CheckStatusEffects(item.StatusEffects) == CheckStatus.Finished)
99  {
100  return CheckStatus.Finished;
101  }
102  }
103  }
104  return CheckStatus.Unfinished;
105  CheckStatus CheckStatusEffects(IEnumerable<StatusEffect> statusEffects)
106  {
107  if (statusEffects == null) { return CheckStatus.Unfinished; }
108  foreach (var statusEffect in statusEffects)
109  {
110  if ((statusEffect.TargetIdentifiers == null || statusEffect.TargetIdentifiers.None()) && !statusEffect.HasConditions) { continue; }
111  switch (TargetItemCondition)
112  {
113  case AIObjectiveLoadItems.ItemCondition.Empty:
114  if (!statusEffect.ReducesItemCondition()) { continue; }
115  break;
116  case AIObjectiveLoadItems.ItemCondition.Full:
117  if (!statusEffect.IncreasesItemCondition()) { continue; }
118  break;
119  default:
120  continue;
121  }
122  useDefaultContainableItemIdentifiers = false;
123  if (statusEffect.TargetIdentifiers != null)
124  {
125  foreach (Identifier target in statusEffect.TargetIdentifiers)
126  {
127  foreach (var prefab in potentialContainablePrefabs)
128  {
129  if (CheckPrefab(prefab, () => prefab.Tags.Contains(target)) == CheckStatus.Finished) { return CheckStatus.Finished; }
130  }
131  }
132  }
133  foreach (var prefab in potentialContainablePrefabs)
134  {
135  if (CheckPrefab(prefab, () => statusEffect.MatchesTagConditionals(prefab)) == CheckStatus.Finished) { return CheckStatus.Finished; }
136  }
137  CheckStatus CheckPrefab(ItemPrefab prefab, Func<bool> isValid)
138  {
139  if (validContainableItemIdentifiers.Contains(prefab.Identifier)) { return CheckStatus.Unfinished; }
140  if (!isValid()) { return CheckStatus.Unfinished; }
141  validContainableItemIdentifiers.Add(prefab.Identifier);
142  if (potentialContainablePrefabs.Any(p => !validContainableItemIdentifiers.Contains(p.Identifier))) { return CheckStatus.Unfinished; }
143  return CheckStatus.Finished;
144  }
145  }
146  return CheckStatus.Unfinished;
147  }
148  }
149  }
150  var identifiers = useDefaultContainableItemIdentifiers ?
151  potentialContainablePrefabs.Select(p => p.Identifier).ToImmutableHashSet() :
152  validContainableItemIdentifiers.ToImmutableHashSet();
153  AllValidContainableItemIdentifiers.Add(Container.Prefab, identifiers);
154  return identifiers;
155  }
156 
157  protected override float GetPriority()
158  {
159  if (!IsAllowed)
160  {
162  return Priority;
163  }
164  else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition))
165  {
166  // Reduce priority to 0 if the this isn't a valid container right now
167  Priority = 0;
168  }
169  else if (targetItem == null)
170  {
171  Priority = 0;
172  }
173  else
174  {
175  float dist = 0.0f;
176  if (character.CurrentHull != targetItem.CurrentHull)
177  {
178  AddDistance(character.WorldPosition, targetItem.WorldPosition);
179  }
180  if (targetItem.CurrentHull != Container.CurrentHull)
181  {
182  AddDistance(targetItem.WorldPosition, Container.WorldPosition);
183  }
184  void AddDistance(Vector2 startPos, Vector2 targetPos)
185  {
186  float yDist = Math.Abs(startPos.Y - targetPos.Y);
187  // If we're on the same level with the target, we'll disregard the vertical distance
188  if (yDist > 100) { dist += yDist * 5; }
189  dist += Math.Abs(character.WorldPosition.X - targetPos.X);
190  }
191 
192  float distanceFactor =
193  GetDistanceFactor(targetItem.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000, factorAtMinDistance: 0.9f, factorAtMaxDistance: 0);
194 
195  bool hasContainable = character.HasItem(targetItem);
196  float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100;
197  float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2);
198  Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
199  if (decontainObjective != null && targetItem.Container != Container)
200  {
201  if (!IsValidContainable(targetItem))
202  {
203  // Target is not valid anymore, abandon the objective
204  decontainObjective.Abandon = true;
205  }
206  else if (!ItemContainer.Inventory.CanBePut(targetItem) && ItemContainer.Inventory.AllItems.None(i => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition)))
207  {
208  // The container is full and there's no item that should be removed, abandon the objective
209  decontainObjective.Abandon = true;
210  }
211  }
213  {
214  // Prioritize containers that still have empty space by lowering the priority of objectives with a full target container
215  Priority /= 4;
216  }
217  }
218  return Priority;
219  }
220 
221  protected override void Act(float deltaTime)
222  {
223  if (targetItem == null)
224  {
225  if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority))
226  {
227  if (item == null)
228  {
229  // No possible containables found, abandon the objective
230  Abandon = true;
231  }
232  targetItem = item;
233  }
234  objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
235  float GetPriority(Item item)
236  {
237  try
238  {
239  // Prefer items closer to full condition when target condition is Empty, and vice versa
240  float conditionBasedPriority = TargetItemCondition switch
241  {
242  AIObjectiveLoadItems.ItemCondition.Full => MathUtils.InverseLerp(100.0f, 0.0f, item.ConditionPercentage),
243  AIObjectiveLoadItems.ItemCondition.Empty => MathUtils.InverseLerp(0.0f, 100.0f, item.ConditionPercentage),
244  _ => throw new NotImplementedException()
245  };
246  // Prefer items that have the same identifier as one of the already contained items
247  return ItemContainer.ContainsItemsWithSameIdentifier(item) ? conditionBasedPriority : conditionBasedPriority / 2;
248  }
249  catch (NotImplementedException)
250  {
251 #if DEBUG
252  DebugConsole.LogError($"Unexpected target condition \"{TargetItemCondition}\" in local function GetConditionBasedProperty");
253 #endif
254  return 0.0f;
255  }
256  }
257  }
258  else
259  {
260  if(decontainObjective == null && !IsValidContainable(targetItem))
261  {
262  IgnoreTargetItem();
263  Reset();
264  return;
265  }
266  TryAddSubObjective(ref decontainObjective,
267  constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
268  {
269  AbandonGetItemDialogueCondition = () => IsValidContainable(targetItem),
270  AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier,
271  Equip = true,
272  RemoveExistingWhenNecessary = true,
273  RemoveExistingPredicate = (i) => !ValidContainableItemIdentifiers.Contains(i.Prefab.Identifier) || AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
274  RemoveExistingMax = 1
275  },
276  onCompleted: () =>
277  {
278  IsCompleted = true;
279  RemoveSubObjective(ref decontainObjective);
280  },
281  onAbandon: () =>
282  {
283  // Try again
284  IgnoreTargetItem();
285  Reset();
286  });
287  }
288  }
289 
290  private bool IsValidContainable(Item item)
291  {
292  if (item == null) { return false; }
293  if (item.Removed) { return false; }
294  if (!ValidContainableItemIdentifiers.Contains(item.Prefab.Identifier)) { return false; }
295  if (ignoredItems.Contains(item)) { return false; }
296  if ((item.Illegitimate) == character.IsOnPlayerTeam) { return false; }
297  if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
298  if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; }
299  Item parentItem = item.Container;
300  while (parentItem != null)
301  {
302  if (parentItem.HasTag(Tags.DontTakeItems)) { return false; }
303  parentItem = parentItem.Container;
304  }
305  if (!item.HasAccess(character)) { return false; }
306  if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; }
307  if (!ItemContainer.CanBeContained(item)) { return false; }
308  if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; }
309  if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full)
310  {
311  // Ignore items that have had their condition increase recently
312  if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full && item.ConditionIncreasedRecently) { return false; }
313  // Ignore items inside their (condition-restricted) primary containers
314  if (item.ParentInventory is ItemInventory itemInventory && item.IsContainerPreferred(itemInventory.Container, out bool _, out bool isSecondary, requireConditionRestriction: true) && !isSecondary) { return false; }
315  }
316  // Ignore items inside another valid container
317  if (AIObjectiveLoadItems.IsValidTarget(item.Container, character, TargetContainerTags)) { return false; }
318  return true;
319  }
320 
321  protected override bool CheckObjectiveState() => IsCompleted;
322 
323  public override void Reset()
324  {
325  base.Reset();
326  // Don't reset the target item when resetting the objective because it affects priority calculations
327  decontainObjective = null;
328  itemIndex = 0;
329  }
330 
331  private void IgnoreTargetItem()
332  {
333  if(targetItem == null) { return; }
334  ignoredItems.Add(targetItem);
335  targetItem = null;
336  }
337  }
338 }
float Priority
Final priority value after all calculations.
bool CanEquip(Item item, bool allowWearing)
static float GetDistanceFactor(Vector2 selfPos, Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier=3, float maxDistance=10000.0f, float factorAtMinDistance=1.0f)
Get a normalized value representing how close the target position is. The value is a rough estimation...
AIObjectiveLoadItem(Item container, ImmutableArray< Identifier > targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, Identifier option, Character character, AIObjectiveManager objectiveManager, float priorityModifier)
override bool CheckObjectiveState()
Should return whether the objective is completed or not.
override void Act(float deltaTime)
static bool ItemMatchesTargetCondition(Item item, ItemCondition targetCondition)
override bool IsValidTarget(Item target)
const float LowestOrderPriority
Maximum priority of an order given to the character (rightmost order in the crew list)
bool HasItem(Item item, bool requireEquipped=false, InvSlotType? slotType=null)
bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable< Identifier > identifiers=null, bool ignoreBroken=true, IEnumerable< Item > ignoredItems=null, IEnumerable< Identifier > ignoredContainerIdentifiers=null, Func< Item, bool > customPredicate=null, Func< Item, float > customPriorityFunction=null, float maxItemDistance=10000, ISpatialEntity positionalReference=null)
Finds the closest item seeking by identifiers or tags from the world. Ignores items that are outside ...
virtual Vector2 WorldPosition
Definition: Entity.cs:49
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
bool CanBePut(Item item)
Can the item be put in the inventory (i.e. is there a suitable free slot or a stack the item can be p...
bool ConditionIncreasedRecently
Return true if the condition of this item increased within the last second.
bool Illegitimate
Item shouldn't be in the player's inventory. If the guards find it, they will consider it as a theft.
bool IsContainerPreferred(ItemContainer container, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRestriction=false)
bool HasAccess(Character character)
Used by the AI to check whether they can (in principle) and are allowed (in practice) to interact wit...
override bool IsFull(bool takeStacksIntoAccount=false)
Is there room to put more items in the inventory. Doesn't take stacking into account by default.
readonly Identifier Identifier
Definition: Prefab.cs:34