Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs
3 using FarseerPhysics;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Collections.Immutable;
8 using System.Globalization;
9 using System.Linq;
10 using System.Xml.Linq;
11 
13 {
15  {
16  readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition, bool BlameEquipperForDeath);
17 
18  readonly record struct ContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation);
19 
20  class SlotRestrictions
21  {
22  public int MaxStackSize;
23  public List<RelatedItem> ContainableItems;
24  public readonly bool AutoInject;
25 
26  public SlotRestrictions(int maxStackSize, List<RelatedItem> containableItems, bool autoInject)
27  {
28  MaxStackSize = maxStackSize;
29  ContainableItems = containableItems;
30  AutoInject = autoInject;
31  }
32 
33  public bool MatchesItem(Item item)
34  {
35  return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(item));
36  }
37 
38  public bool MatchesItem(ItemPrefab itemPrefab)
39  {
40  return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(itemPrefab));
41  }
42 
43  public bool MatchesItem(Identifier identifierOrTag)
44  {
45  return
46  ContainableItems == null || ContainableItems.Count == 0 ||
47  ContainableItems.Any(c => c.Identifiers.Contains(identifierOrTag) && !c.ExcludedIdentifiers.Contains(identifierOrTag));
48  }
49  }
50 
51  public readonly NamedEvent<ItemContainer> OnContainedItemsChanged = new NamedEvent<ItemContainer>();
52 
53  private bool alwaysContainedItemsSpawned;
54 
55  public readonly ItemInventory Inventory;
56 
57  private readonly List<ActiveContainedItem> activeContainedItems = new List<ActiveContainedItem>();
58 
59  private readonly List<ContainedItem> containedItems = new List<ContainedItem>();
60 
61  private List<ushort>[] itemIds;
62 
63  //how many items can be contained
64  private int capacity;
65  [Serialize(5, IsPropertySaveable.No, description: "How many items can be contained inside this item.")]
66  public int Capacity
67  {
68  get { return capacity; }
69  private set
70  {
71  capacity = Math.Max(value, 0);
72  MainContainerCapacity = value;
73  }
74  }
78  public int MainContainerCapacity { get; private set; }
79 
80  //how many items can be contained
81  private int maxStackSize;
82  [Serialize(64, IsPropertySaveable.No, description: "How many items can be stacked in one slot. Does not increase the maximum stack size of the items themselves, e.g. a stack of bullets could have a maximum size of 8 but the number of bullets in a specific weapon could be restricted to 6.")]
83  public int MaxStackSize
84  {
85  get { return maxStackSize; }
86  set { maxStackSize = Math.Max(value, 1); }
87  }
88 
89  private bool hideItems;
90  [Serialize(true, IsPropertySaveable.No, description: "Should the items contained inside this item be hidden."
91  + " If set to false, you should use the ItemPos and ItemInterval properties to determine where the items get rendered.")]
92  public bool HideItems
93  {
94  get { return hideItems; }
95  set
96  {
97  hideItems = value;
98  Drawable = !hideItems;
99  }
100  }
101 
102  [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position where the contained items get drawn at (offset from the upper left corner of the sprite in pixels).")]
103  public Vector2 ItemPos { get; set; }
104 
105  [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The interval at which the contained items are spaced apart from each other (in pixels).")]
106  public Vector2 ItemInterval { get; set; }
107 
108  [Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")]
109  public int ItemsPerRow { get; set; }
110 
111  [Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")]
112  public bool DrawInventory
113  {
114  get;
115  set;
116  }
117 
118  [Serialize(true, IsPropertySaveable.No, "Allow dragging and dropping items to deposit items into this inventory.")]
119  public bool AllowDragAndDrop
120  {
121  get;
122  set;
123  }
124 
125  [Serialize(true, IsPropertySaveable.No)]
127  {
128  get;
129  set;
130  }
131 
132  [Serialize(true, IsPropertySaveable.Yes, description: "When this item is equipped, and you 'quick use' (double click / equip button) another equippable item, should the game attempt to move that item inside this one?")]
133  public bool QuickUseMovesItemsInside { get; set; }
134 
135  [Serialize(false, IsPropertySaveable.No, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")]
137  {
138  get;
139  set;
140  }
141 
142  [Serialize(true, IsPropertySaveable.No)]
143  public bool AllowAccess { get; set; }
144 
145  [Serialize(false, IsPropertySaveable.No)]
146  public bool AccessOnlyWhenBroken { get; set; }
147 
148  [Serialize(true, IsPropertySaveable.No)]
149  public bool AllowAccessWhenDropped { get; set; }
150 
151  [Serialize(5, IsPropertySaveable.No, description: "How many inventory slots the inventory has per row.")]
152  public int SlotsPerRow { get; set; }
153 
154  private readonly HashSet<Identifier> containableRestrictions = new HashSet<Identifier>();
155  [Editable, Serialize("", IsPropertySaveable.Yes, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")]
157  {
158  get { return string.Join(",", containableRestrictions); }
159  set
160  {
161  containableRestrictions.Clear();
162  if (!value.IsNullOrEmpty())
163  {
164  foreach (var str in value.Split(','))
165  {
166  if (str.IsNullOrWhiteSpace()) { continue; }
167  containableRestrictions.Add(str.ToIdentifier());
168  }
169  }
170  }
171  }
172 
173  [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should this container be automatically filled with items?")]
174  public bool AutoFill { get; set; }
175 
176  private float itemRotation;
177  [Serialize(0.0f, IsPropertySaveable.No, description: "The rotation in which the contained sprites are drawn (in degrees).")]
178  public float ItemRotation
179  {
180  get { return MathHelper.ToDegrees(itemRotation); }
181  set { itemRotation = MathHelper.ToRadians(value); }
182  }
183 
184  [Serialize("", IsPropertySaveable.No, description: "Specify an item for the container to spawn with.")]
185  public string SpawnWithId
186  {
187  get;
188  set;
189  }
190 
191  [Serialize(false, IsPropertySaveable.No, description: "Should the items configured using SpawnWithId spawn if this item is broken.")]
193  {
194  get;
195  set;
196  }
197 
198  [Serialize(false, IsPropertySaveable.No, description: "Should the items be injected into the user.")]
199  public bool AutoInject
200  {
201  get;
202  set;
203  }
204 
205  [Serialize(0.5f, IsPropertySaveable.No, description: "The health threshold that the user must reach in order to activate the autoinjection.")]
206  public float AutoInjectThreshold
207  {
208  get;
209  set;
210  }
211 
212  [Serialize(false, IsPropertySaveable.No)]
213  public bool RemoveContainedItemsOnDeconstruct { get; set; }
214 
218  public bool Locked
219  {
220  get { return Inventory.Locked; }
221  set { Inventory.Locked = value; }
222  }
223 
228  {
229  get => Inventory.AllItems.Count();
230  }
231 
236  {
237  get => Inventory.AllItems.Count(it => it.Condition > 0.0f);
238  }
239 
240  public int ExtraStackSize
241  {
242  get => Inventory.ExtraStackSize;
243  set => Inventory.ExtraStackSize = value;
244  }
245 
246  private readonly ImmutableArray<SlotRestrictions> slotRestrictions;
247 
248  readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
249 
250  private float prevContainedItemRefreshRotation;
251  private Vector2 prevContainedItemRefreshPosition;
252 
253  private float autoInjectCooldown = 1.0f;
254  const float AutoInjectInterval = 1.0f;
255 
256  private bool subContainersCanAutoInject;
257 
258 
259  public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
260  {
261  isRestrictionsDefined = containableRestrictions.Any();
262  if (slotRestrictions.None(s => s.MatchesItem(item))) { return false; }
263  if (!isRestrictionsDefined) { return true; }
264  return identifiersOrTags.Any(id => containableRestrictions.Any(r => r == id));
265  }
266 
267  public bool ShouldBeContained(Item item, out bool isRestrictionsDefined)
268  {
269  isRestrictionsDefined = containableRestrictions.Any();
270  if (slotRestrictions.None(s => s.MatchesItem(item))) { return false; }
271  if (!isRestrictionsDefined) { return true; }
272  return containableRestrictions.Any(id => item.Prefab.Identifier == id || item.HasTag(id));
273  }
274 
275  private ImmutableHashSet<Identifier> containableItemIdentifiers;
276  public ImmutableHashSet<Identifier> ContainableItemIdentifiers => containableItemIdentifiers;
277 
278  public List<RelatedItem> ContainableItems { get; }
279  public List<RelatedItem> AllSubContainableItems { get; }
280 
281  public readonly bool HasSubContainers;
282 
283  public bool hasSignalConnections;
284 
285  private string totalConditionValueString = "", totalConditionPercentageString = "", totalItemsString = "";
286  private float prevTotalConditionValue = 0, prevTotalConditionPercentage = 0; int prevTotalItems = 0;
287 
289  : base(item, element)
290  {
291  int totalCapacity = capacity;
292 
293  foreach (var subElement in element.Elements())
294  {
295  switch (subElement.Name.ToString().ToLowerInvariant())
296  {
297  case "containable":
298  RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name);
299  if (containable == null)
300  {
301  DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.",
302  contentPackage: element.ContentPackage);
303  continue;
304  }
305  ContainableItems ??= new List<RelatedItem>();
306  ContainableItems.Add(containable);
307  break;
308  case "subcontainer":
309  totalCapacity += subElement.GetAttributeInt("capacity", 1);
310  HasSubContainers = true;
311  break;
312  }
313  }
314  Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow);
315 
316  // we have to assign this here because the fields are serialized before the inventory is created otherwise
317  ExtraStackSize = element.GetAttributeInt(nameof(ExtraStackSize), 0);
318 
319  List<SlotRestrictions> newSlotRestrictions = new List<SlotRestrictions>(totalCapacity);
320  for (int i = 0; i < capacity; i++)
321  {
322  newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems, autoInject: false));
323  }
324 
325  int subContainerIndex = capacity;
326  foreach (var subElement in element.Elements())
327  {
328  if (subElement.Name.ToString().ToLowerInvariant() != "subcontainer") { continue; }
329 
330  int subCapacity = subElement.GetAttributeInt("capacity", 1);
331  int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize);
332  bool autoInject = subElement.GetAttributeBool("autoinject", false);
333 
334  subContainersCanAutoInject |= autoInject;
335 
336  var subContainableItems = new List<RelatedItem>();
337  foreach (var subSubElement in subElement.Elements())
338  {
339  if (subSubElement.Name.ToString().ToLowerInvariant() != "containable") { continue; }
340 
341  RelatedItem containable = RelatedItem.Load(subSubElement, returnEmpty: false, parentDebugName: item.Name);
342  if (containable == null)
343  {
344  DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.",
345  contentPackage: element.ContentPackage);
346  continue;
347  }
348  subContainableItems.Add(containable);
349  AllSubContainableItems ??= new List<RelatedItem>();
350  AllSubContainableItems.Add(containable);
351  }
352 
353  for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++)
354  {
355  newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems, autoInject));
356  }
357  subContainerIndex += subCapacity;
358  }
359  capacity = totalCapacity;
360  slotRestrictions = newSlotRestrictions.ToImmutableArray();
361  System.Diagnostics.Debug.Assert(totalCapacity == slotRestrictions.Length);
362  InitProjSpecific(element);
363  }
364 
366  {
367  int containableIndex = 0;
368  foreach (var subElement in element.GetChildElements("containable"))
369  {
370  RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name);
371  if (containable == null)
372  {
373  DebugConsole.ThrowError("Error when loading containable restrictions for \"" + item.Name + "\" - containable with no identifiers.",
374  contentPackage: element.ContentPackage);
375  continue;
376  }
377  ContainableItems[containableIndex] = containable;
378  containableIndex++;
379  if (containableIndex >= ContainableItems.Count) { break; }
380  }
381  for (int i = 0; i < capacity; i++)
382  {
383  slotRestrictions[i].ContainableItems = ContainableItems;
384  }
385 #if CLIENT
386  if (element.GetChildElement("clearsubcontainerrestrictions") != null)
387  {
388  for (int i = capacity - MainContainerCapacity; i < capacity; i++)
389  {
390  slotRestrictions[i].MaxStackSize = MaxStackSize;
391  slotIcons[i] = null;
392  }
393  }
394 #endif
395  }
396 
397  public int GetMaxStackSize(int slotIndex)
398  {
399  if (slotIndex < 0 || slotIndex >= capacity)
400  {
401  return 0;
402  }
403  return slotRestrictions[slotIndex].MaxStackSize;
404  }
405 
406  partial void InitProjSpecific(ContentXElement element);
407 
408  public void OnItemContained(Item containedItem)
409  {
410  int index = Inventory.FindIndex(containedItem);
411  RelatedItem relatedItem = null;
412  if (index >= 0 && index < slotRestrictions.Length)
413  {
414  if (slotRestrictions[index].ContainableItems != null)
415  {
416  activeContainedItems.RemoveAll(i => i.Item == containedItem);
417  foreach (var containableItem in slotRestrictions[index].ContainableItems)
418  {
419  if (!containableItem.MatchesItem(containedItem)) { continue; }
420  //the 1st matching ContainableItem of the slot determines the hiding, position and rotation of the item
421  relatedItem ??= containableItem;
422  foreach (StatusEffect effect in containableItem.StatusEffects)
423  {
424  activeContainedItems.Add(new ActiveContainedItem(
425  containedItem,
426  effect,
427  containableItem.ExcludeBroken,
428  containableItem.ExcludeFullCondition,
429  containableItem.BlameEquipperForDeath));
430  }
431  }
432  }
433  }
434 
435  var containedItemInfo = new ContainedItem(containedItem,
436  Hide: relatedItem?.Hide ?? false,
437  ItemPos: relatedItem?.ItemPos,
438  Rotation: relatedItem?.Rotation ?? 0.0f);
439  containedItems.RemoveAll(d => d.Item == containedItem);
440 
441  if (hideItems)
442  {
443  //if the items aren't visible, the draw order doesn't matter and we can skip the sorting
444  containedItems.Add(containedItemInfo);
445  }
446  else
447  {
448  int containedIndex = 0;
449  while (containedIndex < containedItems.Count)
450  {
451  if (index <= Inventory.FindIndex(containedItems[containedIndex].Item))
452  {
453  break;
454  }
455  containedIndex++;
456  }
457  //sort drawables by their order in the inventory
458  containedItems.Insert(containedIndex, containedItemInfo);
459  }
460 
461  if (item.GetComponent<Planter>() != null)
462  {
463  GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningPlanted:" + containedItem.Prefab.Identifier);
464  }
465 
466  //no need to Update() if this item has no statuseffects and no physics body, and if there are no signal connections.
467  IsActive = hasSignalConnections || activeContainedItems.Count > 0 || Inventory.AllItems.Any(static it => it.body != null);
468 
469  if (IsActive && item.GetRootInventoryOwner() is Character owner &&
470  owner.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand)))
471  {
472  // Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it).
473  SetContainedActive(true);
474  }
475  if (containedItem.FlippedX != item.FlippedX)
476  {
477  containedItem.FlipX(relativeToSub: false);
478  }
479  if (containedItem.FlippedY != item.FlippedY)
480  {
481  containedItem.FlipY(relativeToSub: false);
482  }
484  CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
485  OnContainedItemsChanged.Invoke(this);
486  }
487 
488  public override void Move(Vector2 amount, bool ignoreContacts = false)
489  {
491  }
492 
493  public void OnItemRemoved(Item containedItem)
494  {
495  activeContainedItems.RemoveAll(i => i.Item == containedItem);
496  containedItems.RemoveAll(i => i.Item == containedItem);
498  //deactivate if the inventory is empty
499  IsActive = hasSignalConnections || activeContainedItems.Count > 0 || Inventory.AllItems.Any(static it => it.body != null);
500  CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
501  OnContainedItemsChanged.Invoke(this);
502  }
503 
504  public bool BlameEquipperForDeath()
505  {
506  return activeContainedItems.Any(c => c.BlameEquipperForDeath);
507  }
508 
509  public bool CanBeContained(Item item)
510  {
511  if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; }
512  return slotRestrictions.Any(s => s.MatchesItem(item));
513  }
514 
515  public bool CanBeContained(Item item, int index)
516  {
517  if (index < 0 || index >= capacity) { return false; }
518  if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; }
519  return slotRestrictions[index].MatchesItem(item);
520  }
521 
522  public bool CanBeContained(ItemPrefab itemPrefab)
523  {
524  return slotRestrictions.Any(s => s.MatchesItem(itemPrefab));
525  }
526 
527  public bool CanBeContained(ItemPrefab itemPrefab, int index)
528  {
529  if (index < 0 || index >= capacity) { return false; }
530  return slotRestrictions[index].MatchesItem(itemPrefab);
531  }
532 
534  {
535  if (item == null) { return false; }
536  foreach (var containedItem in Inventory.AllItems)
537  {
538  if (containedItem.Prefab.Identifier == item.Prefab.Identifier)
539  {
540  return true;
541  }
542  }
543  return false;
544  }
545 
546  public override void FlipX(bool relativeToSub)
547  {
548  base.FlipX(relativeToSub);
549  if (HideItems) { return; }
550  if (item.body == null) { return; }
551  foreach (Item containedItem in Inventory.AllItems)
552  {
553  if (containedItem.body != null && containedItem.body.Enabled && containedItem.body.Dir != item.body.Dir)
554  {
555  containedItem.FlipX(relativeToSub);
556  }
557  }
558  }
559 
560  public override void Update(float deltaTime, Camera cam)
561  {
562  if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned)
563  {
564  SpawnAlwaysContainedItems();
565  alwaysContainedItemsSpawned = true;
566  }
567 
569  {
570  float totalConditionValue = 0, totalConditionPercentage = 0; int totalItems = 0;
571  foreach (var item in Inventory.AllItems)
572  {
573  if (!MathUtils.NearlyEqual(item.Condition, 0))
574  {
575  totalConditionValue += item.Condition;
576  totalConditionPercentage += item.ConditionPercentage;
577  totalItems++;
578  }
579  }
580 
581  if (!MathUtils.NearlyEqual(totalConditionValue, prevTotalConditionValue))
582  {
583  totalConditionValueString = ((int)totalConditionValue).ToString(CultureInfo.InvariantCulture);
584  prevTotalConditionValue = totalConditionValue;
585  }
586 
587  if (!MathUtils.NearlyEqual(totalConditionPercentage, prevTotalConditionPercentage))
588  {
589  totalConditionPercentageString = ((int)totalConditionPercentage).ToString(CultureInfo.InvariantCulture);
590  prevTotalConditionPercentage = totalConditionPercentage;
591  }
592 
593  if (totalItems != prevTotalItems)
594  {
595  totalItemsString = totalItems.ToString(CultureInfo.InvariantCulture);
596  prevTotalItems = totalItems;
597  }
598 
599  item.SendSignal(totalConditionValueString, "contained_conditions");
600  item.SendSignal(totalConditionPercentageString, "contained_conditions_percentage");
601  item.SendSignal(totalItemsString, "contained_items");
602  }
603 
604  if (item.ParentInventory is CharacterInventory ownerInventory)
605  {
606  SetContainedItemPositionsIfNeeded();
607 
608  if (AutoInject || subContainersCanAutoInject)
609  {
610  //normally autoinjection should delete the (medical) item, so it only gets applied once
611  //but in multiplayer clients aren't allowed to remove items themselves, so they may be able to trigger this dozens of times
612  //before the server notifies them of the item being removed, leading to a sharp lag spike.
613  //this can also happen with mods, if there's a way to autoinject something that doesn't get removed On Use.
614  //so let's ensure the item is only applied once per second at most.
615 
616  autoInjectCooldown -= deltaTime;
617  if (autoInjectCooldown <= 0.0f &&
618  ownerInventory?.Owner is Character ownerCharacter &&
619  ownerCharacter.HealthPercentage / 100f <= AutoInjectThreshold &&
620  ownerCharacter.HasEquippedItem(item))
621  {
622  if (AutoInject)
623  {
624  Inventory.AllItemsMod.ForEach(i => Inject(i));
625  }
626  else
627  {
628  for (int i = 0; i < slotRestrictions.Length; i++)
629  {
630  if (slotRestrictions[i].AutoInject)
631  {
632  Inventory.GetItemsAt(i).ForEachMod(i => Inject(i));
633  }
634  }
635  }
636  void Inject(Item item)
637  {
638  item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter, useTarget: ownerCharacter);
639  item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter, useTarget: ownerCharacter);
640  item.GetComponent<GeneticMaterial>()?.Equip(ownerCharacter);
641  }
642  autoInjectCooldown = AutoInjectInterval;
643  }
644  }
645 
646  }
647  else if (item.body != null && item.body.Enabled)
648  {
649  if (item.body.FarseerBody.Awake)
650  {
651  SetContainedItemPositionsIfNeeded();
652  }
653  }
654  else if (!hasSignalConnections && activeContainedItems.Count == 0)
655  {
656  IsActive = false;
657  return;
658  }
659 
660  foreach (var activeContainedItem in activeContainedItems)
661  {
662  Item contained = activeContainedItem.Item;
663 
664  if (activeContainedItem.ExcludeBroken && contained.Condition <= 0.0f) { continue; }
665  if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; }
666  StatusEffect effect = activeContainedItem.StatusEffect;
667 
668  targets.Clear();
669  bool wearing = item.GetComponent<Wearable>() is Wearable { IsActive: true };
670  if (effect.HasTargetType(StatusEffect.TargetType.This))
671  {
672  targets.AddRange(item.AllPropertyObjects);
673  }
674  if (effect.HasTargetType(StatusEffect.TargetType.Contained))
675  {
676  targets.AddRange(contained.AllPropertyObjects);
677  }
678  if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character)
679  {
680  targets.Add(character);
681  }
682  if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
683  effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
684  {
685  effect.AddNearbyTargets(item.WorldPosition, targets);
686  }
687  effect.Apply(ActionType.OnActive, deltaTime, item, targets);
688  effect.Apply(ActionType.OnContaining, deltaTime, item, targets);
689  if (wearing) { effect.Apply(ActionType.OnWearing, deltaTime, item, targets); }
690  }
691  }
692 
696  private void SetContainedItemPositionsIfNeeded()
697  {
698  if (Vector2.DistanceSquared(prevContainedItemRefreshPosition, item.Position) > 10.0f ||
699  Math.Abs(prevContainedItemRefreshRotation - item.body?.Rotation ?? item.RotationRad) > 0.01f)
700  {
702  prevContainedItemRefreshPosition = item.Position;
703  prevContainedItemRefreshRotation = item.body?.Rotation ?? item.RotationRad;
704  }
705  }
706 
707  public override void UpdateBroken(float deltaTime, Camera cam)
708  {
709  //update when the item is broken too to get OnContaining effects to execute and contained item positions to update
710  if (IsActive)
711  {
712  Update(deltaTime, cam);
713  }
714  }
715 
716  public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
717  {
718  return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
719  }
720 
721  public override bool Select(Character character)
722  {
723  if (!AllowAccess) { return false; }
724  if (item.Container != null) { return false; }
726  {
727  if (item.Condition > 0)
728  {
729  return false;
730  }
731  }
732  if (AutoInteractWithContained && character.SelectedItem == null && Screen.Selected is not { IsEditor: true })
733  {
734  foreach (Item contained in Inventory.AllItems)
735  {
736  if (contained.TryInteract(character))
737  {
738  character.FocusedItem = contained;
739  return false;
740  }
741  }
742  }
743  var abilityItem = new AbilityItemContainer(item);
744  character.CheckTalents(AbilityEffectType.OnOpenItemContainer, abilityItem);
745 
746  if (item.ParentInventory?.Owner == character)
747  {
748  //can't select ItemContainers in the character's inventory (the inventory is drawn by hovering the cursor over the inventory slot, not as a GUIFrame)
749  return false;
750  }
751  else
752  {
753  return base.Select(character);
754  }
755  }
756 
757  public override bool Pick(Character picker)
758  {
759  if (!AllowAccess) { return false; }
761  {
762  if (item.Condition > 0)
763  {
764  return false;
765  }
766  }
767  if (AutoInteractWithContained && Screen.Selected is not { IsEditor: true })
768  {
769  foreach (Item contained in Inventory.AllItems)
770  {
771  if (contained.TryInteract(picker))
772  {
773  picker.FocusedItem = contained;
774  return true;
775  }
776  }
777  }
778 
779  IsActive = true;
780 
781  return picker != null;
782  }
783 
784  public override bool Combine(Item item, Character user)
785  {
786  if (!AllowDragAndDrop && user != null) { return false; }
787  if (!slotRestrictions.Any(s => s.MatchesItem(item))) { return false; }
788  if (user != null && !user.CanAccessInventory(Inventory)) { return false; }
789  //genetic materials use special logic for combining, don't allow doing it by placing them inside each other here
790  if (this.Item.GetComponent<GeneticMaterial>() != null) { return false; }
791 
792  if (Inventory.TryPutItem(item, user))
793  {
794  IsActive = true;
795  if (hideItems && item.body != null) { item.body.Enabled = false; }
796 
797  return true;
798  }
799 
800  return false;
801  }
802 
803  public override void Drop(Character dropper, bool setTransform = true)
804  {
805  IsActive = true;
806  SetContainedActive(false);
807  }
808 
809  public override void Equip(Character character)
810  {
811  IsActive = true;
812  if (character != null && character.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand)))
813  {
814  SetContainedActive(true);
815  }
816  else
817  {
818  SetContainedActive(false);
819  }
820  }
821 
822  private void SetContainedActive(bool active)
823  {
824  if ((ContainableItems == null || !ContainableItems.Any(c => c.SetActive)) &&
825  (AllSubContainableItems == null || !AllSubContainableItems.Any(c => c.SetActive)))
826  {
827  return;
828  }
829  foreach (Item containedItem in Inventory.AllItems)
830  {
831  RelatedItem containableItem = FindContainableItem(containedItem);
832  if (containableItem != null && containableItem.SetActive)
833  {
834  foreach (var ic in containedItem.Components)
835  {
836  ic.IsActive = active;
837  }
838  if (containedItem.body != null)
839  {
840  containedItem.body.Enabled = active;
841  if (active)
842  {
843  containedItem.body.PhysEnabled = false;
844  }
845  }
846  }
847  }
848  if (active)
849  {
850  FlipX(false);
851  }
852  }
853 
854  private RelatedItem FindContainableItem(Item item)
855  {
856  int index = Inventory.FindIndex(item);
857  if (index == -1 ) { return null; }
858  return slotRestrictions[index]?.ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item));
859  }
860 
864  public int? FindSuitableSubContainerIndex(Identifier itemTagOrIdentifier)
865  {
866  for (int i = 0; i < slotRestrictions.Length; i++)
867  {
868  if (slotRestrictions[i].MatchesItem(itemTagOrIdentifier)) { return i; }
869  }
870  return null;
871  }
872 
873  public override void ReceiveSignal(Signal signal, Connection connection)
874  {
875  switch (connection.Name)
876  {
877  case "activate":
878  case "use":
879  case "trigger_in":
880  if (signal.value != "0")
881  {
882  item.Use(1.0f, user: signal.sender);
883  }
884  break;
885  }
886  }
887 
888 #warning There's some code duplication here and in DrawContainedItems() method, but it's not straightforward to get rid of it, because of slightly different logic and the usage of draw positions vs. positions etc. Should probably be splitted into smaller methods.
890  {
891  Vector2 transformedItemPos = ItemPos * item.Scale;
892  Vector2 transformedItemInterval = ItemInterval * item.Scale;
893  Vector2 transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f);
894  Vector2 transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y);
895 
896  if (ItemPos == Vector2.Zero && ItemInterval == Vector2.Zero)
897  {
898  transformedItemPos = item.Position;
899  }
900  else
901  {
902  if (item.body == null)
903  {
904  if (item.FlippedX)
905  {
906  transformedItemPos.X = -transformedItemPos.X;
907  transformedItemPos.X += item.Rect.Width;
908  transformedItemInterval.X = -transformedItemInterval.X;
909  transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
910  }
911  if (item.FlippedY)
912  {
913  transformedItemPos.Y = -transformedItemPos.Y;
914  transformedItemPos.Y -= item.Rect.Height;
915  transformedItemInterval.Y = -transformedItemInterval.Y;
916  transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y;
917  }
918  transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y);
919  if (Math.Abs(item.Rotation) > 0.01f)
920  {
921  Matrix transform = Matrix.CreateRotationZ(-item.RotationRad);
922  transformedItemPos = Vector2.Transform(transformedItemPos - item.Position, transform) + item.Position;
923  transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
924  transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
925  transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
926  }
927  }
928  else
929  {
930  Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
931  if (item.body.Dir == -1.0f)
932  {
933  transformedItemPos.X = -transformedItemPos.X;
934  transformedItemInterval.X = -transformedItemInterval.X;
935  transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
936  }
937  transformedItemPos = Vector2.Transform(transformedItemPos, transform);
938  transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
939  transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
940  transformedItemPos += item.Position;
941  }
942  }
943 
944  int i = 0;
945  Vector2 currentItemPos = transformedItemPos;
946  foreach (ContainedItem contained in containedItems)
947  {
948  Vector2 itemPos = currentItemPos;
949  if (contained.ItemPos.HasValue)
950  {
951  Vector2 pos = contained.ItemPos.Value;
952  if (item.body != null)
953  {
954  Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
955  pos.X *= item.body.Dir;
956  itemPos = Vector2.Transform(pos, transform) + item.body.Position;
957  }
958  else
959  {
960  itemPos = pos;
961  // This code is aped based on above. Not tested.
962  if (item.FlippedX)
963  {
964  itemPos.X = -itemPos.X;
965  itemPos.X += item.Rect.Width;
966  }
967  if (item.FlippedY)
968  {
969  itemPos.Y = -itemPos.Y;
970  itemPos.Y -= item.Rect.Height;
971  }
972  itemPos += new Vector2(item.Rect.X, item.Rect.Y);
973  if (Math.Abs(item.RotationRad) > 0.01f)
974  {
975  Matrix transform = Matrix.CreateRotationZ(item.RotationRad);
976  itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position;
977  }
978  }
979  }
980 
981  if (contained.Item.body != null)
982  {
983  try
984  {
985  Vector2 simPos = ConvertUnits.ToSimUnits(itemPos);
986  float rotation = itemRotation;
987  if (contained.Rotation != 0)
988  {
989  rotation = MathHelper.ToRadians(contained.Rotation);
990  }
991  if (item.body != null)
992  {
993  rotation *= item.body.Dir;
994  rotation += item.body.Rotation;
995  }
996  else
997  {
998  rotation += -item.RotationRad;
999  }
1000  contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
1001  contained.Item.body.SetPrevTransform(contained.Item.body.SimPosition, contained.Item.body.Rotation);
1002  contained.Item.body.UpdateDrawPosition();
1003  }
1004  catch (Exception e)
1005  {
1006  DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace());
1007  GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Item.Name,
1008  GameAnalyticsManager.ErrorSeverity.Error,
1009  "SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace());
1010  }
1011  contained.Item.body.Submarine = item.Submarine;
1012  }
1013 
1014  contained.Item.Rect =
1015  new Rectangle(
1016  (int)(itemPos.X - contained.Item.Rect.Width / 2.0f),
1017  (int)(itemPos.Y + contained.Item.Rect.Height / 2.0f),
1018  contained.Item.Rect.Width, contained.Item.Rect.Height);
1019 
1020  contained.Item.Submarine = item.Submarine;
1021  contained.Item.CurrentHull = item.CurrentHull;
1022  contained.Item.SetContainedItemPositions();
1023 
1024  i++;
1025  if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f)
1026  {
1027  //interval set on both axes -> use a grid layout
1028  currentItemPos += transformedItemIntervalHorizontal;
1029  if (i % ItemsPerRow == 0)
1030  {
1031  currentItemPos = transformedItemPos;
1032  currentItemPos += transformedItemIntervalVertical * (i / ItemsPerRow);
1033  }
1034  }
1035  else
1036  {
1037  currentItemPos += transformedItemInterval;
1038  }
1039  }
1040  }
1041 
1042  public override void OnItemLoaded()
1043  {
1045  containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty<Identifier>()).ToImmutableHashSet();
1046  hasSignalConnections = item.Connections?.Any(c => c.Name is "contained_conditions" or "contained_conditions_percentage" or "contained_items") ?? false;
1047  if (item.Submarine == null || !item.Submarine.Loading)
1048  {
1049  SpawnAlwaysContainedItems();
1050  }
1051  }
1052 
1053  public override void OnMapLoaded()
1054  {
1055  if (itemIds != null)
1056  {
1057  for (ushort i = 0; i < itemIds.Length; i++)
1058  {
1059  if (i >= Inventory.Capacity)
1060  {
1061  //legacy support: before item stacking was implemented, revolver for example had a separate slot for each bullet
1062  //now there's just one, try to put the extra items where they fit (= stack them)
1063  Inventory.TryPutItem(item, user: null, createNetworkEvent: false);
1064  continue;
1065  }
1066  foreach (ushort id in itemIds[i])
1067  {
1068  if (!(Entity.FindEntityByID(id) is Item item)) { continue; }
1069  Inventory.TryPutItem(item, i, false, false, null, createNetworkEvent: false, ignoreCondition: true);
1070  }
1071  }
1072  itemIds = null;
1073  }
1074 
1075  //outpost and ruins are loaded in multiple stages (each module is loaded separately)
1076  //spawning items at this point during the generation will cause ID overlaps with the entities in the modules loaded afterwards
1077  //so let's not spawn them at this point, but in the 1st Update()
1079  {
1080  if (SpawnWithId.Length > 0)
1081  {
1082  IsActive = true;
1083  }
1084  }
1085  else
1086  {
1087  SpawnAlwaysContainedItems();
1088  }
1089  }
1090 
1091  private void SpawnAlwaysContainedItems()
1092  {
1093  if (SpawnWithId.Length > 0 && (item.Condition > 0.0f || SpawnWithIdWhenBroken))
1094  {
1095  string[] splitIds = SpawnWithId.Split(',');
1096  foreach (string id in splitIds)
1097  {
1098  ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == id);
1099  if (prefab != null && Inventory != null && Inventory.CanBePut(prefab))
1100  {
1101  bool isEditor = false;
1102 #if CLIENT
1103  isEditor = Screen.Selected == GameMain.SubEditorScreen;
1104 #endif
1105  if (!isEditor && (Entity.Spawner == null || Entity.Spawner.Removed) && GameMain.NetworkMember == null)
1106  {
1107  var spawnedItem = new Item(prefab, Vector2.Zero, null);
1108  Inventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots, createNetworkEvent: false);
1109  alwaysContainedItemsSpawned = true;
1110  }
1111  else
1112  {
1113  IsActive = true;
1114  Entity.Spawner?.AddItemToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false, onSpawned: (Item item) => { alwaysContainedItemsSpawned = true; });
1115  }
1116  }
1117  }
1118  }
1119  }
1120 
1121  protected override void ShallowRemoveComponentSpecific()
1122  {
1123  }
1124 
1125  protected override void RemoveComponentSpecific()
1126  {
1127  base.RemoveComponentSpecific();
1128 #if CLIENT
1129  inventoryTopSprite?.Remove();
1130  inventoryBackSprite?.Remove();
1131  inventoryBottomSprite?.Remove();
1133 
1135  {
1137  return;
1138  }
1139 #endif
1140  //if we're unloading the whole sub, no need to drop anything (everything's going to be removed anyway)
1141  if (!Submarine.Unloading)
1142  {
1143  Inventory.AllItemsMod.ForEach(it => it.Drop(null));
1144  }
1145  }
1146 
1147  public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
1148  {
1149  base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
1150 
1151  string containedString = componentElement.GetAttributeString("contained", "");
1152  string[] itemIdStrings = containedString.Split(',');
1153  itemIds = new List<ushort>[itemIdStrings.Length];
1154  for (int i = 0; i < itemIdStrings.Length; i++)
1155  {
1156  itemIds[i] ??= new List<ushort>();
1157  foreach (string idStr in itemIdStrings[i].Split(';'))
1158  {
1159  if (!int.TryParse(idStr, out int id)) { continue; }
1160  itemIds[i].Add(idRemap.GetOffsetId(id));
1161  }
1162  }
1163  ExtraStackSize = componentElement.GetAttributeInt(nameof(ExtraStackSize), 0);
1164  }
1165 
1166  public override XElement Save(XElement parentElement)
1167  {
1168  XElement componentElement = base.Save(parentElement);
1169  string[] itemIdStrings = new string[Inventory.Capacity];
1170  for (int i = 0; i < Inventory.Capacity; i++)
1171  {
1172  var items = Inventory.GetItemsAt(i);
1173  itemIdStrings[i] = string.Join(';', items.Select(it => it.ID.ToString()));
1174  }
1175  componentElement.Add(new XAttribute("contained", string.Join(',', itemIdStrings)));
1176  componentElement.Add(new XAttribute(nameof(ExtraStackSize), ExtraStackSize));
1177  return componentElement;
1178  }
1179  }
1180 
1182  {
1184  {
1185  Item = item;
1186  }
1187  public Item Item { get; set; }
1188  }
1189 }
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel=CharacterInventory.AccessLevel.Limited)
readonly? ContentPackage ContentPackage
Definition: ContentPath.cs:21
string? GetAttributeString(string key, string? def)
int GetAttributeInt(string key, int def)
static EntitySpawner Spawner
Definition: Entity.cs:31
Submarine Submarine
Definition: Entity.cs:53
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition, float? condition=null, int? quality=null, Action< Item > onSpawned=null)
static GameSession?? GameSession
Definition: GameMain.cs:88
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static NetworkMember NetworkMember
Definition: GameMain.cs:190
readonly Identifier Identifier
ushort GetOffsetId(XElement element)
Definition: IdRemap.cs:86
virtual bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
void DeleteAllItems()
Deletes all items inside the inventory (and also recursively all items inside the items)
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
int FindIndex(Item item)
Find the index of the first slot the item is contained in.
IEnumerable< Item > GetItemsAt(int index)
Get all the item stored in the specified inventory slot. Can return more than one item if the slot co...
IEnumerable< Item > AllItemsMod
All items contained in the inventory. Allows modifying the contents of the inventory while being enum...
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...
void ApplyStatusEffects(ActionType type, float deltaTime, Character character=null, Limb limb=null, Entity useTarget=null, bool isNetworkEvent=false, Vector2? worldPosition=null)
Executes all StatusEffects of the specified type. Note that condition checks are ignored here: that s...
IReadOnlyList< ISerializableEntity > AllPropertyObjects
void Use(float deltaTime, Character user=null, Limb targetLimb=null, Entity useTarget=null, Character userForOnUsedEvent=null)
override void FlipX(bool relativeToSub)
Flip the entity horizontally
override void FlipY(bool relativeToSub)
Flip the entity vertically
bool TryInteract(Character user, bool ignoreRequiredItems=false, bool forceSelectKey=false, bool forceUseKey=false)
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id=Entity.NullEntityID, bool callOnItemLoaded=true)
void SendSignal(string signal, string connectionName)
static readonly PrefabCollection< ItemPrefab > Prefabs
The base class for components holding the different functionalities of the item
override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg=null)
override void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
override bool Pick(Character picker)
a Character has picked the item
int? FindSuitableSubContainerIndex(Identifier itemTagOrIdentifier)
Returns the index of the first slot whose restrictions match the specified tag or identifier
override void Drop(Character dropper, bool setTransform=true)
a Character has dropped the item
int MainContainerCapacity
The capacity of the main container without taking the sub containers into account....
readonly Identifier Identifier
Definition: Prefab.cs:34
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
void AddNearbyTargets(Vector2 worldPosition, List< ISerializableEntity > targets)
virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition=null)
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
AbilityEffectType
Definition: Enums.cs:125