Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Item.cs
3 using FarseerPhysics;
4 using FarseerPhysics.Dynamics;
5 using FarseerPhysics.Dynamics.Contacts;
6 using Microsoft.Xna.Framework;
7 using System;
8 using System.Collections.Concurrent;
9 using System.Collections.Generic;
10 using System.Globalization;
11 using System.Linq;
12 using System.Xml.Linq;
15 using MoonSharp.Interpreter;
16 using System.Collections.Immutable;
17 using Barotrauma.Abilities;
18 
19 #if CLIENT
20 using Microsoft.Xna.Framework.Graphics;
21 #endif
22 
23 namespace Barotrauma
24 {
26  {
27  public static readonly List<Item> ItemList = new List<Item>();
28 
29  private static readonly HashSet<Item> dangerousItems = new HashSet<Item>();
30 
31  public static IReadOnlyCollection<Item> DangerousItems { get { return dangerousItems; } }
32 
33  private static readonly List<Item> repairableItems = new List<Item>();
34 
38  public static IReadOnlyCollection<Item> RepairableItems => repairableItems;
39 
40  private static readonly List<Item> cleanableItems = new List<Item>();
41 
45  public static IReadOnlyCollection<Item> CleanableItems => cleanableItems;
46 
47  private static readonly HashSet<Item> deconstructItems = new HashSet<Item>();
48 
52  public static HashSet<Item> DeconstructItems => deconstructItems;
53 
54  private static readonly List<Item> sonarVisibleItems = new List<Item>();
55 
59  public static IReadOnlyCollection<Item> SonarVisibleItems => sonarVisibleItems;
60 
61  public new ItemPrefab Prefab => base.Prefab as ItemPrefab;
62 
63  public static bool ShowLinks = true;
64 
65  private readonly HashSet<Identifier> tags;
66 
67  private readonly bool isWire, isLogic;
68 
69  private Hull currentHull;
71  {
72  get { return currentHull; }
73  set
74  {
75  currentHull = value;
76  }
77  }
78 
79  public float HullOxygenPercentage
80  {
81  get { return CurrentHull?.OxygenPercentage ?? 0.0f; }
82  }
83 
84  private CampaignMode.InteractionType campaignInteractionType = CampaignMode.InteractionType.None;
86  {
87  get { return campaignInteractionType; }
88  }
89 
90  public void AssignCampaignInteractionType(CampaignMode.InteractionType interactionType, IEnumerable<Client> targetClients = null)
91  {
92  if (campaignInteractionType == interactionType) { return; }
93  campaignInteractionType = interactionType;
94  AssignCampaignInteractionTypeProjSpecific(campaignInteractionType, targetClients);
95  }
96 
97 
98  partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable<Client> targetClients);
99 
100  public bool Visible = true;
101 
102 #if CLIENT
104 #endif
105 
106  //components that determine the functionality of the item
107  private readonly Dictionary<Type, ItemComponent> componentsByType = new Dictionary<Type, ItemComponent>();
108  private readonly List<ItemComponent> components;
112  private readonly List<ItemComponent> updateableComponents = new List<ItemComponent>();
113  private readonly List<IDrawableComponent> drawableComponents;
114  private bool hasComponentsToDraw;
115 
120  public bool FullyInitialized { get; private set; }
121 
123  private readonly float originalWaterDragCoefficient;
124  private float? overrideWaterDragCoefficient;
125  public float WaterDragCoefficient
126  {
127  get => overrideWaterDragCoefficient ?? originalWaterDragCoefficient;
128  set => overrideWaterDragCoefficient = value;
129  }
130 
135  {
136  get { return body?.BodyType ?? BodyType.Dynamic; }
137  set
138  {
139  if (body != null)
140  {
141  body.BodyType = value;
142  }
143  }
144  }
145 
149  public void ResetWaterDragCoefficient() => overrideWaterDragCoefficient = null;
150 
151  public readonly XElement StaticBodyConfig;
152 
153  public List<Fixture> StaticFixtures = new List<Fixture>();
154 
155  private bool transformDirty = true;
156 
157  private static readonly List<Item> itemsWithPendingConditionUpdates = new List<Item>();
158 
159  private float lastSentCondition;
160  private float sendConditionUpdateTimer;
161 
162  private float prevCondition;
163  private float condition;
164 
165  private bool inWater;
166  private readonly bool hasInWaterStatusEffects;
167  private readonly bool hasNotInWaterStatusEffects;
168 
169  private Inventory parentInventory;
170  private readonly ItemInventory ownInventory;
171 
172  private Rectangle defaultRect;
177  {
178  get { return defaultRect; }
179  set { defaultRect = value; }
180  }
181 
182  private readonly Dictionary<string, Connection> connections;
183 
184  private readonly List<Repairable> repairables;
185 
186  private readonly Quality qualityComponent;
187 
188  private ConcurrentQueue<float> impactQueue;
189 
190  //a dictionary containing lists of the status effects in all the components of the item
191  private readonly bool[] hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length];
192  private readonly Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
193 
197  private readonly float conditionMultiplierCampaign = 1.0f;
198 
199  public Action OnInteract;
200 
201  public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; protected set; }
202 
203  private bool? hasInGameEditableProperties;
204  bool HasInGameEditableProperties
205  {
206  get
207  {
208  if (hasInGameEditableProperties == null)
209  {
210  hasInGameEditableProperties = false;
211  if (SerializableProperties.Values.Any(p => p.Attributes.OfType<InGameEditable>().Any()))
212  {
213  hasInGameEditableProperties = true;
214  }
215  else
216  {
217  foreach (ItemComponent component in components)
218  {
219  if (!component.AllowInGameEditing) { continue; }
220  if (component.SerializableProperties.Values.Any(p => p.Attributes.OfType<InGameEditable>().Any())
221  || component.SerializableProperties.Values.Any(p => p.Attributes.OfType<ConditionallyEditable>().Any(a => a.IsEditable(this))))
222  {
223  hasInGameEditableProperties = true;
224  break;
225  }
226  }
227  }
228  }
229  return (bool)hasInGameEditableProperties;
230  }
231  }
232 
233  public bool EditableWhenEquipped { get; set; } = false;
234 
240 
241  //the inventory in which the item is contained in
243  {
244  get
245  {
246  return parentInventory;
247  }
248  set
249  {
250  parentInventory = value;
251  if (parentInventory != null)
252  {
253  Container = parentInventory.Owner as Item;
254  RemoveFromDroppedStack(allowClientExecute: false);
255  }
256 #if SERVER
257  PreviousParentInventory = value;
258 #endif
259  }
260  }
261 
262  private Item rootContainer;
264  {
265  get { return rootContainer; }
266  private set
267  {
268  if (value == this)
269  {
270  DebugConsole.ThrowError($"Attempted to set the item \"{Prefab.Identifier}\" as it's own root container!\n{Environment.StackTrace.CleanupStackTrace()}");
271  rootContainer = null;
272  return;
273  }
274  rootContainer = value;
275  }
276  }
277 
278  private bool inWaterProofContainer;
279 
280  private Item container;
282  {
283  get { return container; }
284  private set
285  {
286  if (value != container)
287  {
288  container = value;
289  CheckCleanable();
290  SetActiveSprite();
291  RefreshRootContainer();
292  }
293  }
294  }
295 
301  public override string Name
302  {
303  get { return base.Prefab.Name.Value; }
304  }
305 
306  private string description;
307  public string Description
308  {
309  get { return description ?? base.Prefab.Description.Value; }
310  set { description = value; }
311  }
312 
313  private string descriptionTag;
314 
315  [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork)]
319  public string DescriptionTag
320  {
321  get { return descriptionTag; }
322  set
323  {
324  if (value == descriptionTag) { return; }
325  if (value.IsNullOrEmpty())
326  {
327  descriptionTag = null;
328  description = null;
329  }
330  else
331  {
332  description = TextManager.Get(value).Value;
333  descriptionTag = value;
334  }
335  if (FullyInitialized &&
336  SerializableProperties != null &&
337  SerializableProperties.TryGetValue(nameof(DescriptionTag).ToIdentifier(), out SerializableProperty property))
338  {
339  GameMain.NetworkMember?.CreateEntityEvent(this, new ChangePropertyEventData(property, this));
340  }
341  }
342  }
343 
344  [Editable, Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
345  public bool NonInteractable
346  {
347  get;
348  set;
349  }
350 
354  [Editable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, item is interactable only for characters on non-player teams.", alwaysUseInstanceValues: true)]
356  {
357  get;
358  set;
359  }
360 
361  [ConditionallyEditable(ConditionallyEditable.ConditionType.IsSwappableItem), Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
362  public bool AllowSwapping
363  {
364  get;
365  set;
366  }
367 
368  [Serialize(false, IsPropertySaveable.Yes)]
369  public bool PurchasedNewSwap
370  {
371  get;
372  set;
373  }
374 
379  {
380  get
381  {
383  }
384  }
385 
389  public bool IsInteractable(Character character)
390  {
391 #if CLIENT
393  {
394  return true;
395  }
396 #endif
397  if (IsHidden) { return false; }
398  if (character != null && character.IsOnPlayerTeam)
399  {
401  }
402  else
403  {
404  return !NonInteractable;
405  }
406  }
407 
408  public float RotationRad { get; private set; }
409 
410  [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)]
411  public float Rotation
412  {
413  get
414  {
415  return MathHelper.ToDegrees(RotationRad);
416  }
417  set
418  {
419  if (!Prefab.AllowRotatingInEditor) { return; }
420  RotationRad = MathUtils.WrapAnglePi(MathHelper.ToRadians(value));
421 #if CLIENT
423  {
425  foreach (var light in GetComponents<LightComponent>())
426  {
427  light.SetLightSourceTransform();
428  }
429  foreach (var turret in GetComponents<Turret>())
430  {
431  turret.UpdateLightComponents();
432  }
433  }
434 #endif
435  }
436  }
437 
438  public float ImpactTolerance
439  {
440  get { return Prefab.ImpactTolerance; }
441  }
442 
443  public float InteractDistance
444  {
445  get { return Prefab.InteractDistance; }
446  }
447 
448  public float InteractPriority
449  {
450  get { return Prefab.InteractPriority; }
451  }
452 
453  public override Vector2 Position
454  {
455  get
456  {
457  return (body == null) ? base.Position : body.Position;
458  }
459  }
460 
461  public override Vector2 SimPosition
462  {
463  get
464  {
465  return (body == null) ? ConvertUnits.ToSimUnits(base.Position) : body.SimPosition;
466  }
467  }
468 
470  {
471  get
472  {
473  return WorldRect;
474  }
475  }
476 
477  private float scale = 1.0f;
478  public override float Scale
479  {
480  get { return scale; }
481  set
482  {
483  if (scale == value) { return; }
484  scale = MathHelper.Clamp(value, Prefab.MinScale, Prefab.MaxScale);
485 
486  float relativeScale = scale / base.Prefab.Scale;
487 
489  {
490  int newWidth = ResizeHorizontal ? rect.Width : (int)(defaultRect.Width * relativeScale);
491  int newHeight = ResizeVertical ? rect.Height : (int)(defaultRect.Height * relativeScale);
492  Rect = new Rectangle(rect.X, rect.Y, newWidth, newHeight);
493  }
494 
495  if (components != null)
496  {
497  foreach (ItemComponent component in components)
498  {
499  component.OnScaleChanged();
500  }
501  }
502  }
503  }
504 
506  {
507  get;
508  set;
509  } = float.PositiveInfinity;
510 
511  protected Color spriteColor;
512  [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)]
513  public Color SpriteColor
514  {
515  get { return spriteColor; }
516  set { spriteColor = value; }
517  }
518 
520  public Color InventoryIconColor
521  {
522  get;
523  protected set;
524  }
525 
526  [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true."),
528  public Color ContainerColor
529  {
530  get;
531  protected set;
532  }
533 
537  public Identifier ContainerIdentifier
538  {
539  get
540  {
541  return
543  ParentInventory?.Owner?.ToIdentifier() ??
544  Identifier.Empty;
545  }
546  }
547 
551  public bool IsContained
552  {
553  get
554  {
555  return parentInventory != null;
556  }
557  }
558 
562  public float Speed
563  {
564  get
565  {
566  if (body != null && body.PhysEnabled)
567  {
568  return body.LinearVelocity.Length();
569  }
570  else if (ParentInventory?.Owner is Character character)
571  {
572  return character.AnimController.MainLimb.LinearVelocity.Length();
573  }
574  else if (container != null)
575  {
576  return container.Speed;
577  }
578  return 0.0f;
579  }
580  }
581 
582  public Color? HighlightColor;
583 
587  [Serialize("", IsPropertySaveable.Yes)]
588  public string SonarLabel
589  {
590  get { return AiTarget?.SonarLabel?.Value ?? ""; }
591  set
592  {
593  if (AiTarget != null)
594  {
595  string trimmedStr = !string.IsNullOrEmpty(value) && value.Length > 250 ? value.Substring(250) : value;
596  AiTarget.SonarLabel = TextManager.Get(trimmedStr).Fallback(trimmedStr);
597  }
598  }
599  }
600 
604  public bool PhysicsBodyActive
605  {
606  get
607  {
608  return body != null && body.Enabled;
609  }
610  }
611 
615  [Serialize(0.0f, IsPropertySaveable.No)]
616  public new float SoundRange
617  {
618  get { return aiTarget == null ? 0.0f : aiTarget.SoundRange; }
619  set { if (aiTarget != null) { aiTarget.SoundRange = Math.Max(0.0f, value); } }
620  }
621 
625  [Serialize(0.0f, IsPropertySaveable.No)]
626  public new float SightRange
627  {
628  get { return aiTarget == null ? 0.0f : aiTarget.SightRange; }
629  set { if (aiTarget != null) { aiTarget.SightRange = Math.Max(0.0f, value); } }
630  }
631 
635  [Serialize(false, IsPropertySaveable.No)]
636  public bool IsShootable { get; set; }
637 
641  [Serialize(false, IsPropertySaveable.No)]
642  public bool RequireAimToUse
643  {
644  get; set;
645  }
646 
650  [Serialize(true, IsPropertySaveable.No)]
652  {
653  get; set;
654  }
655 
659  public bool DontCleanUp
660  {
661  get; set;
662  }
663 
664  public Color Color
665  {
666  get { return spriteColor; }
667  }
668 
669  public bool IsFullCondition { get; private set; }
670  public float MaxCondition { get; private set; }
671  public float ConditionPercentage { get; private set; }
672 
677  {
678  get
679  {
680  float defaultMaxCondition = MaxCondition / MaxRepairConditionMultiplier;
681  return MathUtils.Percentage(Condition, defaultMaxCondition);
682  }
683  }
684 
685  private float offsetOnSelectedMultiplier = 1.0f;
686 
687  [Serialize(1.0f, IsPropertySaveable.No)]
689  {
690  get => offsetOnSelectedMultiplier;
691  set => offsetOnSelectedMultiplier = value;
692  }
693 
694  private float healthMultiplier = 1.0f;
695 
696  [Serialize(1.0f, IsPropertySaveable.Yes, "Multiply the maximum condition by this value")]
697  public float HealthMultiplier
698  {
699  get => healthMultiplier;
700  set
701  {
702  float prevConditionPercentage = ConditionPercentage;
703  healthMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity);
705  condition = MaxCondition * prevConditionPercentage / 100.0f;
707  }
708  }
709 
710  private float maxRepairConditionMultiplier = 1.0f;
711 
712  [Serialize(1.0f, IsPropertySaveable.Yes)]
714  {
715  get => maxRepairConditionMultiplier;
716  set
717  {
718  maxRepairConditionMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity);
720  }
721  }
722 
723  [Serialize(false, IsPropertySaveable.Yes)]
724  private bool HasBeenInstantiatedOnce { get; set; }
725 
726  //the default value should be Prefab.Health, but because we can't use it in the attribute,
727  //we'll just use NaN (which does nothing) and set the default value in the constructor/load
728  [Serialize(float.NaN, IsPropertySaveable.No), Editable]
729  public float Condition
730  {
731  get { return condition; }
732  set
733  {
734  SetCondition(value, isNetworkEvent: false);
735  }
736  }
737 
738  private double ConditionLastUpdated { get; set; }
739  private float LastConditionChange { get; set; }
743  public bool ConditionIncreasedRecently => (Timing.TotalTime < ConditionLastUpdated + 1.0f) && LastConditionChange > 0.0f;
744 
745  public float Health
746  {
747  get { return condition; }
748  }
749 
750  private bool? indestructible;
754  public bool Indestructible
755  {
756  get => indestructible ?? Prefab.Indestructible;
757  set => indestructible = value;
758  }
759 
760  public bool AllowDeconstruct
761  {
762  get;
763  set;
764  }
765 
766  [Editable, Serialize(false, isSaveable: IsPropertySaveable.Yes, "When enabled will prevent the item from taking damage from all sources")]
767  public bool InvulnerableToDamage { get; set; }
768 
773  public bool StolenDuringRound;
774 
779 
780  private bool spawnedInCurrentOutpost;
782  {
783  get { return spawnedInCurrentOutpost; }
784  set
785  {
786  if (!spawnedInCurrentOutpost && value)
787  {
789  }
790  spawnedInCurrentOutpost = value;
791  }
792  }
793 
794  private bool allowStealing;
795  [Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true,
796  description: $"Determined by where/how the item originally spawned. If ItemPrefab.AllowStealing is true, stealing the item is always allowed.")]
797  public bool AllowStealing
798  {
799  get { return allowStealing || Prefab.AllowStealingAlways; }
800  set { allowStealing = value; }
801  }
802 
803  public bool IsSalvageMissionItem;
804 
805  private string originalOutpost;
806  [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
807  public string OriginalOutpost
808  {
809  get { return originalOutpost; }
810  set
811  {
812  originalOutpost = value;
813  if (!string.IsNullOrEmpty(value) &&
815  GameMain.GameSession?.LevelData?.Seed == value)
816  {
817  spawnedInCurrentOutpost = true;
818  }
819  }
820  }
821 
823  public string Tags
824  {
825  get { return string.Join(",", tags); }
826  set
827  {
828  tags.Clear();
829  // Always add prefab tags
830  base.Prefab.Tags.ForEach(t => tags.Add(t));
831  if (!string.IsNullOrWhiteSpace(value))
832  {
833  string[] splitTags = value.Split(',');
834  foreach (string tag in splitTags)
835  {
836  string[] splitTag = tag.Trim().Split(':');
837  splitTag[0] = splitTag[0].ToLowerInvariant();
838  tags.Add(string.Join(":", splitTag).ToIdentifier());
839  }
840  }
841  }
842  }
843 
844  [Serialize(false, IsPropertySaveable.No)]
845  public bool FireProof
846  {
847  get; private set;
848  }
849 
850  private bool waterProof;
851  [Serialize(false, IsPropertySaveable.No)]
852  public bool WaterProof
853  {
854  get { return waterProof; }
855  private set
856  {
857  if (waterProof == value) { return; }
858  waterProof = value;
859  foreach (Item containedItem in ContainedItems)
860  {
861  containedItem.RefreshInWaterProofContainer();
862  }
863  }
864  }
865 
867  {
868  get { return Prefab.UseInHealthInterface; }
869  }
870 
871  public int Quality
872  {
873  get
874  {
875  return qualityComponent?.QualityLevel ?? 0;
876  }
877  set
878  {
879  if (qualityComponent != null)
880  {
881  qualityComponent.QualityLevel = value;
882  }
883  }
884  }
885 
886  public bool InWater
887  {
888  get
889  {
890  //if the item has an active physics body, inWater is updated in the Update method
891  if (body != null && body.Enabled) { return inWater; }
892  if (hasInWaterStatusEffects) { return inWater; }
893 
894  //if not, we'll just have to check
895  return IsInWater();
896  }
897  }
898 
902  public List<Connection> LastSentSignalRecipients
903  {
904  get;
905  private set;
906  } = new List<Connection>(20);
907 
909 
910  //which type of inventory slots (head, torso, any, etc) the item can be placed in
911  private readonly HashSet<InvSlotType> allowedSlots = new HashSet<InvSlotType>();
912  public IEnumerable<InvSlotType> AllowedSlots
913  {
914  get
915  {
916  return allowedSlots;
917  }
918  }
919 
920  public List<Connection> Connections
921  {
922  get
923  {
924  ConnectionPanel panel = GetComponent<ConnectionPanel>();
925  if (panel == null) return null;
926  return panel.Connections;
927  }
928  }
929 
930  public IEnumerable<Item> ContainedItems
931  {
932  get
933  {
934  if (OwnInventories.Length < 2)
935  {
936  if (OwnInventory == null) { yield break; }
937 
938  foreach (var item in OwnInventory.AllItems)
939  {
940  yield return item;
941  }
942  }
943  else
944  {
945  foreach (var inventory in OwnInventories)
946  {
947  foreach (var item in inventory.AllItems)
948  {
949  yield return item;
950  }
951  }
952  }
953  }
954  }
955 
957  {
958  get { return ownInventory; }
959  }
960 
961  public readonly ImmutableArray<ItemInventory> OwnInventories = ImmutableArray<ItemInventory>.Empty;
962 
963  [Editable, Serialize(false, IsPropertySaveable.Yes, description:
964  "Enable if you want to display the item HUD side by side with another item's HUD, when linked together. " +
965  "Disclaimer: It's possible or even likely that the views block each other, if they were not designed to be viewed together!")]
966  public bool DisplaySideBySideWhenLinked { get; set; }
967 
968  public List<Repairable> Repairables
969  {
970  get { return repairables; }
971  }
972 
973  public List<ItemComponent> Components
974  {
975  get { return components; }
976  }
977 
978  public override bool Linkable
979  {
980  get { return Prefab.Linkable; }
981  }
982 
983  public float WorldPositionX => WorldPosition.X;
984  public float WorldPositionY => WorldPosition.Y;
985 
989  public float PositionX
990  {
991  get { return Position.X; }
992  private set
993  {
994  Move(new Vector2(value * Scale, 0.0f));
995  }
996  }
1000  public float PositionY
1001  {
1002  get { return Position.Y; }
1003  private set
1004  {
1005  Move(new Vector2(0.0f, value * Scale));
1006  }
1007  }
1008 
1009  public BallastFloraBranch Infector { get; set; }
1010 
1011  public ItemPrefab PendingItemSwap { get; set; }
1012 
1013  public readonly HashSet<ItemPrefab> AvailableSwaps = new HashSet<ItemPrefab>();
1014 
1015  public override string ToString()
1016  {
1017  return Name + " (ID: " + ID + ")";
1018  }
1019 
1020  private readonly List<ISerializableEntity> allPropertyObjects = new List<ISerializableEntity>();
1021  public IReadOnlyList<ISerializableEntity> AllPropertyObjects
1022  {
1023  get { return allPropertyObjects; }
1024  }
1025 
1026  public bool IgnoreByAI(Character character) => HasTag(Barotrauma.Tags.ItemIgnoredByAI) || OrderedToBeIgnored && character.IsOnPlayerTeam;
1027  public bool OrderedToBeIgnored { get; set; }
1028 
1030  {
1031  get
1032  {
1033  return CurrentHull?.BallastFlora != null;
1034  }
1035  }
1036 
1038  {
1039  get
1040  {
1041  if (CurrentHull?.BallastFlora == null) { return false; }
1042  return CurrentHull.BallastFlora.ClaimedTargets.Contains(this);
1043  }
1044  }
1045 
1046  public bool InPlayerSubmarine => Submarine?.Info is { IsPlayer: true };
1047  public bool InBeaconStation => Submarine?.Info is { Type: SubmarineType.BeaconStation };
1048 
1049  public bool IsLadder { get; }
1050 
1051  public bool IsSecondaryItem { get; }
1052 
1053  private ItemStatManager statManager;
1054  public ItemStatManager StatManager
1055  {
1056  get
1057  {
1058  statManager ??= new ItemStatManager(this);
1059  return statManager;
1060  }
1061  }
1062 
1066  public float LastEatenTime { get; set; }
1067 
1068  public Action<Character> OnDeselect;
1069 
1070  public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true)
1071  : this(new Rectangle(
1072  (int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale),
1073  (int)(position.Y + itemPrefab.Sprite.size.Y / 2 * itemPrefab.Scale),
1074  (int)(itemPrefab.Sprite.size.X * itemPrefab.Scale),
1075  (int)(itemPrefab.Sprite.size.Y * itemPrefab.Scale)),
1076  itemPrefab, submarine, callOnItemLoaded, id: id)
1077  {
1078 
1079  }
1080 
1085  public Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool callOnItemLoaded = true, ushort id = Entity.NullEntityID)
1086  : base(itemPrefab, submarine, id)
1087  {
1088  spriteColor = base.Prefab.SpriteColor;
1089 
1090  components = new List<ItemComponent>();
1091  drawableComponents = new List<IDrawableComponent>(); hasComponentsToDraw = false;
1092  tags = new HashSet<Identifier>();
1093  repairables = new List<Repairable>();
1094 
1095  defaultRect = newRect;
1096  rect = newRect;
1097 
1098  condition = MaxCondition = prevCondition = Prefab.Health;
1099  ConditionPercentage = 100.0f;
1100 
1101  lastSentCondition = condition;
1102 
1103  AllowDeconstruct = itemPrefab.AllowDeconstruct;
1104 
1105  allPropertyObjects.Add(this);
1106 
1107  ContentXElement element = itemPrefab.ConfigElement;
1108  if (element == null) return;
1109 
1111 
1112  if (submarine == null || !submarine.Loading) { FindHull(); }
1113 
1114  SetActiveSprite();
1115 
1116  ContentXElement bodyElement = null;
1117  foreach (var subElement in element.Elements())
1118  {
1119  switch (subElement.Name.ToString().ToLowerInvariant())
1120  {
1121  case "body":
1122  bodyElement = subElement;
1123  float density = subElement.GetAttributeFloat("density", Physics.NeutralDensity);
1124  float minDensity = subElement.GetAttributeFloat("mindensity", density);
1125  float maxDensity = subElement.GetAttributeFloat("maxdensity", density);
1126  if (minDensity < maxDensity)
1127  {
1128  var rand = new Random(ID);
1129  density = MathHelper.Lerp(minDensity, maxDensity, (float)rand.NextDouble());
1130  }
1131 
1132  string collisionCategoryStr = subElement.GetAttributeString("collisioncategory", null);
1133 
1134  Category collisionCategory = Physics.CollisionItem;
1135  Category collidesWith = Physics.DefaultItemCollidesWith;
1136  if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons || Prefab.DamagedByRepairTools) && Condition > 0)
1137  {
1138  //force collision category to Character to allow projectiles and weapons to hit
1139  //(we could also do this by making the projectiles and weapons hit CollisionItem
1140  //and check if the collision should be ignored in the OnCollision callback, but
1141  //that'd make the hit detection more expensive because every item would be included)
1142  collisionCategory = Physics.CollisionCharacter;
1143  collidesWith |= Physics.CollisionProjectile;
1144  }
1145  if (collisionCategoryStr != null)
1146  {
1147  if (!Physics.TryParseCollisionCategory(collisionCategoryStr, out Category cat))
1148  {
1149  DebugConsole.ThrowError("Invalid collision category in item \"" + Name + "\" (" + collisionCategoryStr + ")",
1150  contentPackage: element.ContentPackage);
1151  }
1152  else
1153  {
1154  collisionCategory = cat;
1155  if (cat.HasFlag(Physics.CollisionCharacter))
1156  {
1157  collisionCategory |= Physics.CollisionProjectile;
1158  }
1159  }
1160  }
1161  body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density, collisionCategory, collidesWith, findNewContacts: false);
1162  body.FarseerBody.AngularDamping = subElement.GetAttributeFloat("angulardamping", 0.2f);
1163  body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f);
1164  body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f);
1165  body.UserData = this;
1166  break;
1167  case "trigger":
1168  case "inventoryicon":
1169  case "sprite":
1170  case "deconstruct":
1171  case "brokensprite":
1172  case "decorativesprite":
1173  case "upgradepreviewsprite":
1174  case "price":
1175  case "levelcommonness":
1176  case "suitabletreatment":
1177  case "containedsprite":
1178  case "fabricate":
1179  case "fabricable":
1180  case "fabricableitem":
1181  case "upgrade":
1182  case "preferredcontainer":
1183  case "upgrademodule":
1184  case "upgradeoverride":
1185  case "minimapicon":
1186  case "infectedsprite":
1187  case "damagedinfectedsprite":
1188  case "swappableitem":
1189  case "skillrequirementhint":
1190  break;
1191  case "staticbody":
1192  StaticBodyConfig = subElement;
1193  break;
1194  case "aitarget":
1195  aiTarget = new AITarget(this, subElement);
1196  break;
1197  default:
1198  ItemComponent ic = ItemComponent.Load(subElement, this);
1199  if (ic == null) break;
1200 
1201  AddComponent(ic);
1202  break;
1203  }
1204  }
1205 
1206  foreach (ItemComponent ic in components)
1207  {
1208  if (ic is Pickable pickable)
1209  {
1210  foreach (var allowedSlot in pickable.AllowedSlots)
1211  {
1212  allowedSlots.Add(allowedSlot);
1213  }
1214  }
1215 
1216  if (ic is Repairable repairable) { repairables.Add(repairable); }
1217 
1218  if (ic is IDrawableComponent && ic.Drawable)
1219  {
1220  drawableComponents.Add(ic as IDrawableComponent);
1221  hasComponentsToDraw = true;
1222  }
1223 
1224  if (ic.statusEffectLists == null) { continue; }
1225  if (ic.InheritStatusEffects)
1226  {
1227  // Inherited status effects are added when the ItemComponent is initialized at ItemComponent.cs:332.
1228  // Don't create duplicate effects here.
1229  continue;
1230  }
1231 
1232  statusEffectLists ??= new Dictionary<ActionType, List<StatusEffect>>();
1233 
1234  //go through all the status effects of the component
1235  //and add them to the corresponding statuseffect list
1236  foreach (List<StatusEffect> componentEffectList in ic.statusEffectLists.Values)
1237  {
1238  ActionType actionType = componentEffectList.First().type;
1239  if (!statusEffectLists.TryGetValue(actionType, out List<StatusEffect> statusEffectList))
1240  {
1241  statusEffectList = new List<StatusEffect>();
1242  statusEffectLists.Add(actionType, statusEffectList);
1243  hasStatusEffectsOfType[(int)actionType] = true;
1244  }
1245 
1246  foreach (StatusEffect effect in componentEffectList)
1247  {
1248  statusEffectList.Add(effect);
1249  }
1250  }
1251  }
1252 
1253  hasInWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.InWater];
1254  hasNotInWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.NotInWater];
1255 
1256  if (body != null)
1257  {
1258  body.Submarine = submarine;
1259  originalWaterDragCoefficient = bodyElement.GetAttributeFloat("waterdragcoefficient", 5.0f);
1260  }
1261 
1262  //cache connections into a dictionary for faster lookups
1263  var connectionPanel = GetComponent<ConnectionPanel>();
1264  if (connectionPanel != null)
1265  {
1266  connections = new Dictionary<string, Connection>();
1267  foreach (Connection c in connectionPanel.Connections)
1268  {
1269  if (!connections.ContainsKey(c.Name))
1270  connections.Add(c.Name, c);
1271  }
1272  }
1273 
1274  if (body != null)
1275  {
1276  body.FarseerBody.OnCollision += OnCollision;
1277  }
1278 
1279  var itemContainer = GetComponent<ItemContainer>();
1280  if (itemContainer != null)
1281  {
1282  ownInventory = itemContainer.Inventory;
1283  }
1284 
1285  OwnInventories = GetComponents<ItemContainer>().Select(ic => ic.Inventory).ToImmutableArray();
1286 
1287  qualityComponent = GetComponent<Quality>();
1288 
1289  IsLadder = GetComponent<Ladder>() != null;
1290  IsSecondaryItem = IsLadder || GetComponent<Controller>() is { IsSecondaryItem: true };
1291 
1292  InitProjSpecific();
1293 
1294  if (callOnItemLoaded)
1295  {
1296  foreach (ItemComponent ic in components)
1297  {
1298  ic.OnItemLoaded();
1299  }
1300  }
1301 
1302  var holdables = components.Where(c => c is Holdable);
1303  if (holdables.Count() > 1)
1304  {
1305  DebugConsole.AddWarning($"Item {Prefab.Identifier} has multiple {nameof(Holdable)} components ({string.Join(", ", holdables.Select(h => h.GetType().Name))}).",
1307  }
1308 
1309  InsertToList();
1310  ItemList.Add(this);
1311  if (Prefab.IsDangerous) { dangerousItems.Add(this); }
1312  if (Repairables.Any()) { repairableItems.Add(this); }
1313  if (Prefab.SonarSize > 0.0f) { sonarVisibleItems.Add(this); }
1314  CheckCleanable();
1315 
1316  DebugConsole.Log("Created " + Name + " (" + ID + ")");
1317 
1318  if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; }
1319  if (HasTag(Barotrauma.Tags.LogicItem)) { isLogic = true; }
1320 
1321  GameMain.LuaCs.Hook.Call("item.created", this);
1322 
1323  ApplyStatusEffects(ActionType.OnSpawn, 1.0f);
1324 
1325  // Set max condition multipliers from campaign settings for RecalculateConditionValues()
1326  if (GameMain.GameSession?.Campaign is CampaignMode campaign)
1327  {
1328  if (HasTag(Barotrauma.Tags.OxygenSource))
1329  {
1330  conditionMultiplierCampaign *= campaign.Settings.OxygenMultiplier;
1331  }
1332  if (HasTag(Barotrauma.Tags.Fuel))
1333  {
1334  conditionMultiplierCampaign *= campaign.Settings.FuelMultiplier;
1335  }
1336  }
1337  if (!HasBeenInstantiatedOnce)
1338  {
1339  // This only needs to be done on the very first instantiation.
1340  // MaxCondition will be multiplied in RecalculateConditionValues(), ensuring
1341  // that Condition will stay in line with the multiplier from then on.
1342  condition *= conditionMultiplierCampaign;
1343  }
1344 
1346 
1347  if (callOnItemLoaded)
1348  {
1349  FullyInitialized = true;
1350  }
1351 #if CLIENT
1353 #endif
1354  HasBeenInstantiatedOnce = true; // Enable executing certain things only once
1355  }
1356 
1357  partial void InitProjSpecific();
1358 
1359  public bool IsContainerPreferred(ItemContainer container, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRestriction = false)
1360  => Prefab.IsContainerPreferred(this, container, out isPreferencesDefined, out isSecondary, requireConditionRestriction);
1361 
1362  public override MapEntity Clone()
1363  {
1364  Item clone = new Item(rect, Prefab, Submarine, callOnItemLoaded: false)
1365  {
1366  defaultRect = defaultRect
1367  };
1368  foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
1369  {
1370  if (property.Value.Attributes.OfType<Serialize>().None()) { continue; }
1371  clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
1372  }
1373 
1374  if (components.Count != clone.components.Count)
1375  {
1376  string errorMsg = "Error while cloning item \"" + Name + "\" - clone does not have the same number of components. ";
1377  errorMsg += "Original components: " + string.Join(", ", components.Select(c => c.GetType().ToString()));
1378  errorMsg += ", cloned components: " + string.Join(", ", clone.components.Select(c => c.GetType().ToString()));
1379  DebugConsole.ThrowError(errorMsg);
1380  GameAnalyticsManager.AddErrorEventOnce("Item.Clone:" + Name, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1381  }
1382 
1383  for (int i = 0; i < components.Count && i < clone.components.Count; i++)
1384  {
1385  //order the properties to get them to be applied in a consistent order (may matter for properties that are interconnected somehow, like IsOn/IsActive)
1386  foreach (KeyValuePair<Identifier, SerializableProperty> property in components[i].SerializableProperties.OrderBy(s => s.Key))
1387  {
1388  if (property.Value.Attributes.OfType<Serialize>().None()) { continue; }
1389  clone.components[i].SerializableProperties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i]));
1390  }
1391 
1392  //clone requireditem identifiers
1393  foreach (var kvp in components[i].RequiredItems)
1394  {
1395  for (int j = 0; j < kvp.Value.Count; j++)
1396  {
1397  if (!clone.components[i].RequiredItems.ContainsKey(kvp.Key) ||
1398  clone.components[i].RequiredItems[kvp.Key].Count <= j)
1399  {
1400  continue;
1401  }
1402 
1403  clone.components[i].RequiredItems[kvp.Key][j].JoinedIdentifiers =
1404  kvp.Value[j].JoinedIdentifiers;
1405  }
1406  }
1407  }
1408 
1409  if (FlippedX) { clone.FlipX(false); }
1410  if (FlippedY) { clone.FlipY(false); }
1411 
1412  // Flipping an item tampers with its rotation, so restore it
1413  clone.Rotation = Rotation;
1414 
1415  foreach (ItemComponent component in clone.components)
1416  {
1417  component.OnItemLoaded();
1418  }
1419 
1420  Dictionary<ushort, Item> clonedContainedItems = new();
1421 
1422  for (int i = 0; i < components.Count && i < clone.components.Count; i++)
1423  {
1424  ItemComponent component = components[i],
1425  cloneComp = clone.components[i];
1426 
1427  if (component is not ItemContainer origInv ||
1428  cloneComp is not ItemContainer cloneInv)
1429  {
1430  continue;
1431  }
1432 
1433  foreach (var containedItem in origInv.Inventory.AllItems)
1434  {
1435  var containedClone = (Item)containedItem.Clone();
1436 
1437  cloneInv.Inventory.TryPutItem(containedClone, null);
1438  clonedContainedItems.Add(containedItem.ID, containedClone);
1439  }
1440  }
1441 
1442  for (int i = 0; i < components.Count && i < clone.components.Count; i++)
1443  {
1444  ItemComponent component = components[i],
1445  cloneComp = clone.components[i];
1446 
1447  if (component is not CircuitBox origBox || cloneComp is not CircuitBox cloneBox)
1448  {
1449  continue;
1450  }
1451 
1452  cloneBox.CloneFrom(origBox, clonedContainedItems);
1453  }
1454 
1455  clone.FullyInitialized = true;
1456  return clone;
1457  }
1458 
1459  public void AddComponent(ItemComponent component)
1460  {
1461  allPropertyObjects.Add(component);
1462  components.Add(component);
1463 
1464  if (component.IsActive || component.UpdateWhenInactive || component.Parent != null || (component.IsActiveConditionals != null && component.IsActiveConditionals.Any()))
1465  {
1466  updateableComponents.Add(component);
1467  }
1468 
1469  component.OnActiveStateChanged += (bool isActive) =>
1470  {
1471  bool needsSoundUpdate = false;
1472 #if CLIENT
1473  needsSoundUpdate = component.NeedsSoundUpdate();
1474 #endif
1475  //component doesn't need to be updated if it isn't active, doesn't have a parent that could activate it,
1476  //nor sounds or conditionals that would need to run
1477  if (!isActive && !component.UpdateWhenInactive &&
1478  !needsSoundUpdate &&
1479  component.Parent == null &&
1480  (component.IsActiveConditionals == null || !component.IsActiveConditionals.Any()))
1481  {
1482  if (updateableComponents.Contains(component)) { updateableComponents.Remove(component); }
1483  }
1484  else
1485  {
1486  if (!updateableComponents.Contains(component))
1487  {
1488  updateableComponents.Add(component);
1489  this.isActive = true;
1490  }
1491  }
1492  };
1493 
1494  Type type = component.GetType();
1495  if (!componentsByType.ContainsKey(type))
1496  {
1497  componentsByType.Add(type, component);
1498  Type baseType = type.BaseType;
1499  while (baseType != null && baseType != typeof(ItemComponent))
1500  {
1501  if (!componentsByType.ContainsKey(baseType))
1502  {
1503  componentsByType.Add(baseType, component);
1504  }
1505  baseType = baseType.BaseType;
1506  }
1507  }
1508  }
1509 
1511  {
1512  if (!drawableComponents.Contains(drawable))
1513  {
1514  drawableComponents.Add(drawable);
1515  hasComponentsToDraw = true;
1516 #if CLIENT
1518  cachedVisibleExtents = null;
1519 #endif
1520  }
1521  }
1522 
1524  {
1525  if (drawableComponents.Contains(drawable))
1526  {
1527  drawableComponents.Remove(drawable);
1528  hasComponentsToDraw = drawableComponents.Count > 0;
1529 #if CLIENT
1530  cachedVisibleExtents = null;
1531 #endif
1532  }
1533  }
1534 
1535  public int GetComponentIndex(ItemComponent component)
1536  {
1537  return components.IndexOf(component);
1538  }
1539 
1540  public T GetComponent<T>() where T : ItemComponent
1541  {
1542  if (componentsByType.TryGetValue(typeof(T), out ItemComponent component))
1543  {
1544  return (T)component;
1545  }
1546  if (typeof(T) == typeof(ItemComponent))
1547  {
1548  return (T)components.FirstOrDefault();
1549  }
1550  return default;
1551  }
1552 
1553  public IEnumerable<T> GetComponents<T>()
1554  {
1555  if (typeof(T) == typeof(ItemComponent))
1556  {
1557  return components.Cast<T>();
1558  }
1559  if (!componentsByType.ContainsKey(typeof(T))) { return Enumerable.Empty<T>(); }
1560  return components.Where(c => c is T).Cast<T>();
1561  }
1562 
1563  public float GetQualityModifier(Quality.StatType statType)
1564  {
1565  return GetComponent<Quality>()?.GetValue(statType) ?? 0.0f;
1566  }
1567 
1568  public void RemoveContained(Item contained)
1569  {
1570  ownInventory?.RemoveItem(contained);
1571  contained.Container = null;
1572  }
1573 
1574  public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = true, bool setPrevTransform = true)
1575  {
1576  if (!MathUtils.IsValid(simPosition))
1577  {
1578  string errorMsg =
1579  "Attempted to move the item " + Name +
1580  " to an invalid position (" + simPosition + ")\n" + Environment.StackTrace.CleanupStackTrace();
1581 
1582  DebugConsole.ThrowError(errorMsg);
1583  GameAnalyticsManager.AddErrorEventOnce(
1584  "Item.SetPosition:InvalidPosition" + ID,
1585  GameAnalyticsManager.ErrorSeverity.Error,
1586  errorMsg);
1587  return;
1588  }
1589 
1590  if (body != null)
1591  {
1592 #if DEBUG
1593  try
1594  {
1595 #endif
1596  if (body.PhysEnabled)
1597  {
1598  body.SetTransform(simPosition, rotation, setPrevTransform);
1599  }
1600  else
1601  {
1602  body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform);
1603  }
1604 #if DEBUG
1605  }
1606  catch (Exception e)
1607  {
1608  DebugConsole.ThrowError("Failed to set item transform", e);
1609  }
1610 #endif
1611  }
1612 
1613  Vector2 displayPos = ConvertUnits.ToDisplayUnits(simPosition);
1614 
1615  rect.X = (int)MathF.Round(displayPos.X - rect.Width / 2.0f);
1616  rect.Y = (int)MathF.Round(displayPos.Y + rect.Height / 2.0f);
1617 
1618  if (findNewHull) { FindHull(); }
1619  }
1620 
1624  public bool AllowDroppingOnSwapWith(Item otherItem)
1625  {
1626  if (!Prefab.AllowDroppingOnSwap || otherItem == null) { return false; }
1627  if (Prefab.AllowDroppingOnSwapWith.Any())
1628  {
1629  foreach (Identifier tagOrIdentifier in Prefab.AllowDroppingOnSwapWith)
1630  {
1631  if (otherItem.Prefab.Identifier == tagOrIdentifier) { return true; }
1632  if (otherItem.HasTag(tagOrIdentifier)) { return true; }
1633  }
1634  return false;
1635  }
1636  else
1637  {
1638  return true;
1639  }
1640  }
1641 
1642  public void SetActiveSprite()
1643  {
1644  SetActiveSpriteProjSpecific();
1645  }
1646 
1647  partial void SetActiveSpriteProjSpecific();
1648 
1652  public void CheckCleanable()
1653  {
1654  var pickable = GetComponent<Pickable>();
1655  if (pickable != null && !pickable.IsAttached &&
1656  Prefab.PreferredContainers.Any() &&
1657  (container == null || container.HasTag(Barotrauma.Tags.AllowCleanup)))
1658  {
1659  if (!cleanableItems.Contains(this))
1660  {
1661  cleanableItems.Add(this);
1662  }
1663  }
1664  else
1665  {
1666  cleanableItems.Remove(this);
1667  }
1668  }
1669 
1670  public override void Move(Vector2 amount, bool ignoreContacts = true)
1671  {
1672  if (!MathUtils.IsValid(amount))
1673  {
1674  DebugConsole.ThrowError($"Attempted to move an item by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}");
1675  return;
1676  }
1677 
1678  base.Move(amount, ignoreContacts);
1679 
1680  if (ItemList != null && body != null)
1681  {
1682  if (ignoreContacts)
1683  {
1684  body.SetTransformIgnoreContacts(body.SimPosition + ConvertUnits.ToSimUnits(amount), body.Rotation);
1685  }
1686  else
1687  {
1688  body.SetTransform(body.SimPosition + ConvertUnits.ToSimUnits(amount), body.Rotation);
1689  }
1690  }
1691  foreach (ItemComponent ic in components)
1692  {
1693  ic.Move(amount, ignoreContacts);
1694  }
1695 
1696  if (body != null && (Submarine == null || !Submarine.Loading)) { FindHull(); }
1697  }
1698 
1699  public Rectangle TransformTrigger(Rectangle trigger, bool world = false)
1700  {
1701  Rectangle baseRect = world ? WorldRect : Rect;
1702 
1703  Rectangle transformedRect =
1704  new Rectangle(
1705  (int)(baseRect.X + trigger.X * Scale),
1706  (int)(baseRect.Y + trigger.Y * Scale),
1707  (trigger.Width == 0) ? Rect.Width : (int)(trigger.Width * Scale),
1708  (trigger.Height == 0) ? Rect.Height : (int)(trigger.Height * Scale));
1709 
1710  if (FlippedX)
1711  {
1712  transformedRect.X = baseRect.X + (baseRect.Right - transformedRect.Right);
1713  }
1714  if (FlippedY)
1715  {
1716  transformedRect.Y = baseRect.Y + ((baseRect.Y - baseRect.Height) - (transformedRect.Y - transformedRect.Height));
1717  }
1718 
1719  return transformedRect;
1720  }
1721 
1722  public override Quad2D GetTransformedQuad()
1723  => Quad2D.FromSubmarineRectangle(rect).Rotated(-RotationRad);
1724 
1728  public static void UpdateHulls()
1729  {
1730  foreach (Item item in ItemList)
1731  {
1732  item.FindHull();
1733  }
1734  }
1735 
1736  public Hull FindHull()
1737  {
1738  if (parentInventory != null && parentInventory.Owner != null)
1739  {
1740  if (parentInventory.Owner is Character character)
1741  {
1742  CurrentHull = character.AnimController.CurrentHull;
1743  }
1744  else if (parentInventory.Owner is Item item)
1745  {
1746  CurrentHull = item.CurrentHull;
1747  }
1748 
1749  Submarine = parentInventory.Owner.Submarine;
1750  if (body != null) { body.Submarine = Submarine; }
1751 
1752  return CurrentHull;
1753  }
1754 
1756  if (body != null && body.Enabled && (body.BodyType == BodyType.Dynamic || Submarine == null))
1757  {
1760  }
1761 
1762  return CurrentHull;
1763  }
1764 
1765  private void RefreshRootContainer()
1766  {
1767  Item newRootContainer = null;
1768  inWaterProofContainer = false;
1769  if (Container != null)
1770  {
1771  Item rootContainer = Container;
1772  inWaterProofContainer |= Container.WaterProof;
1773 
1774  while (rootContainer.Container != null)
1775  {
1776  rootContainer = rootContainer.Container;
1777  if (rootContainer == this)
1778  {
1779  DebugConsole.ThrowError($"Invalid container hierarchy: \"{Prefab.Identifier}\" was contained inside itself!\n{Environment.StackTrace.CleanupStackTrace()}");
1780  rootContainer = null;
1781  break;
1782  }
1783  inWaterProofContainer |= rootContainer.WaterProof;
1784  }
1785  newRootContainer = rootContainer;
1786  }
1787  if (newRootContainer != RootContainer)
1788  {
1789  RootContainer = newRootContainer;
1790  isActive = true;
1791  foreach (Item containedItem in ContainedItems)
1792  {
1793  containedItem.RefreshRootContainer();
1794  }
1795  }
1796  }
1797 
1798  private void RefreshInWaterProofContainer()
1799  {
1800  inWaterProofContainer = false;
1801  if (container == null) { return; }
1802  if (container.WaterProof || container.inWaterProofContainer)
1803  {
1804  inWaterProofContainer = true;
1805  }
1806  foreach (Item containedItem in ContainedItems)
1807  {
1808  containedItem.RefreshInWaterProofContainer();
1809  }
1810  }
1811 
1816  public bool HasAccess(Character character)
1817  {
1818  if (character.IsBot && IgnoreByAI(character)) { return false; }
1819  if (!IsInteractable(character)) { return false; }
1820  var itemContainer = GetComponent<ItemContainer>();
1821  if (itemContainer != null && !itemContainer.HasAccess(character)) { return false; }
1822  if (Container != null && !Container.HasAccess(character)) { return false; }
1823  if (GetComponent<Pickable>() is { CanBePicked: false }) { return false; }
1824  return true;
1825  }
1826 
1827  public bool IsOwnedBy(Entity entity) => FindParentInventory(i => i.Owner == entity) != null;
1828 
1830  {
1831  if (ParentInventory == null) { return this; }
1834  return RootContainer ?? this;
1835  }
1836 
1837  public Inventory FindParentInventory(Func<Inventory, bool> predicate)
1838  {
1839  if (parentInventory != null)
1840  {
1841  if (predicate(parentInventory))
1842  {
1843  return parentInventory;
1844  }
1845  if (parentInventory.Owner is Item owner)
1846  {
1847  return owner.FindParentInventory(predicate);
1848  }
1849  }
1850  return null;
1851  }
1852 
1854  {
1855  foreach (ItemComponent component in components)
1856  {
1857  (component as ItemContainer)?.SetContainedItemPositions();
1858  }
1859  }
1860 
1861  public void AddTag(string tag)
1862  {
1863  AddTag(tag.ToIdentifier());
1864  }
1865 
1866  public void AddTag(Identifier tag)
1867  {
1868  if (tags.Contains(tag)) { return; }
1869  tags.Add(tag);
1870  }
1871 
1872  public void RemoveTag(Identifier tag)
1873  {
1874  if (!tags.Contains(tag)) { return; }
1875  tags.Remove(tag);
1876  }
1877 
1878  public bool HasTag(Identifier tag)
1879  {
1880  if (tag == null) { return true; }
1881  return tags.Contains(tag) || base.Prefab.Tags.Contains(tag);
1882  }
1883 
1884  public bool HasIdentifierOrTags(IEnumerable<Identifier> identifiersOrTags)
1885  {
1886  if (identifiersOrTags == null) { return false; }
1887  if (identifiersOrTags.Contains(Prefab.Identifier)) { return true; }
1888  foreach (Identifier tag in identifiersOrTags)
1889  {
1890  if (HasTag(tag)) { return true; }
1891  }
1892  return false;
1893  }
1894 
1895  public void ReplaceTag(string tag, string newTag)
1896  {
1897  ReplaceTag(tag.ToIdentifier(), newTag.ToIdentifier());
1898  }
1899 
1900  public void ReplaceTag(Identifier tag, Identifier newTag)
1901  {
1902  if (!tags.Contains(tag)) { return; }
1903  tags.Remove(tag);
1904  tags.Add(newTag);
1905  }
1906 
1907  public IReadOnlyCollection<Identifier> GetTags()
1908  {
1909  return tags;
1910  }
1911 
1912  public bool HasTag(IEnumerable<Identifier> allowedTags)
1913  {
1914  if (allowedTags == null) return true;
1915  foreach (Identifier tag in allowedTags)
1916  {
1917  if (tags.Contains(tag)) return true;
1918  }
1919  return false;
1920  }
1921 
1922  public bool ConditionalMatches(PropertyConditional conditional)
1923  {
1924  return ConditionalMatches(conditional, checkContainer: true);
1925  }
1926 
1927  public bool ConditionalMatches(PropertyConditional conditional, bool checkContainer)
1928  {
1929  if (checkContainer)
1930  {
1931  if (conditional.TargetContainer)
1932  {
1933  if (conditional.TargetGrandParent)
1934  {
1935  return container?.container != null && container.container.ConditionalMatches(conditional, checkContainer: false);
1936  }
1937  return container != null && container.ConditionalMatches(conditional, checkContainer: false);
1938  }
1939  }
1940  if (string.IsNullOrEmpty(conditional.TargetItemComponent))
1941  {
1942  if (!conditional.Matches(this)) { return false; }
1943  }
1944  else
1945  {
1946  foreach (ItemComponent component in components)
1947  {
1948  if (component.Name != conditional.TargetItemComponent) { continue; }
1949  if (!conditional.Matches(component)) { return false; }
1950  }
1951  }
1952  return true;
1953  }
1954 
1958  public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, Vector2? worldPosition = null)
1959  {
1960  if (!hasStatusEffectsOfType[(int)type]) { return; }
1961 
1962  foreach (StatusEffect effect in statusEffectLists[type])
1963  {
1964  ApplyStatusEffect(effect, type, deltaTime, character, limb, useTarget, isNetworkEvent, checkCondition: false, worldPosition);
1965  }
1966  }
1967 
1968  readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
1969 
1970  public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, bool checkCondition = true, Vector2? worldPosition = null)
1971  {
1972  if (effect.ShouldWaitForInterval(this, deltaTime)) { return; }
1973  if (!isNetworkEvent && checkCondition)
1974  {
1975  if (condition == 0.0f && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { return; }
1976  }
1977  if (effect.type != type) { return; }
1978 
1979  bool hasTargets = effect.TargetIdentifiers == null;
1980 
1981  targets.Clear();
1982 
1983  if (effect.HasTargetType(StatusEffect.TargetType.Contained))
1984  {
1985  foreach (Item containedItem in ContainedItems)
1986  {
1987  if (effect.TargetIdentifiers != null &&
1988  !effect.TargetIdentifiers.Contains(((MapEntity)containedItem).Prefab.Identifier) &&
1989  !effect.TargetIdentifiers.Any(id => containedItem.HasTag(id)))
1990  {
1991  continue;
1992  }
1993 
1994  if (effect.TargetSlot > -1)
1995  {
1996  if (!OwnInventory.GetItemsAt(effect.TargetSlot).Contains(containedItem)) { continue; }
1997  }
1998 
1999  hasTargets = true;
2000  targets.Add(containedItem);
2001  }
2002  }
2003 
2004  if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters) || effect.HasTargetType(StatusEffect.TargetType.NearbyItems))
2005  {
2006  effect.AddNearbyTargets(WorldPosition, targets);
2007  if (targets.Count > 0)
2008  {
2009  hasTargets = true;
2010  }
2011  }
2012 
2013  if (effect.HasTargetType(StatusEffect.TargetType.UseTarget) && useTarget is ISerializableEntity serializableTarget)
2014  {
2015  hasTargets = true;
2016  targets.Add(serializableTarget);
2017  }
2018 
2019  if (!hasTargets) { return; }
2020 
2021  if (effect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null)
2022  {
2023  targets.Add(CurrentHull);
2024  }
2025 
2026  if (effect.HasTargetType(StatusEffect.TargetType.This))
2027  {
2028  foreach (var pobject in AllPropertyObjects)
2029  {
2030  targets.Add(pobject);
2031  }
2032  }
2033 
2034  if (character != null)
2035  {
2036  if (effect.HasTargetType(StatusEffect.TargetType.Character))
2037  {
2038  if (type == ActionType.OnContained && ParentInventory is CharacterInventory characterInventory)
2039  {
2040  targets.Add(characterInventory.Owner as ISerializableEntity);
2041  }
2042  else
2043  {
2044  targets.Add(character);
2045  }
2046  }
2047  if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
2048  {
2049  targets.AddRange(character.AnimController.Limbs);
2050  }
2051  if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb == null && effect.targetLimbs != null)
2052  {
2053  foreach (var characterLimb in character.AnimController.Limbs)
2054  {
2055  if (effect.targetLimbs.Contains(characterLimb.type)) { targets.Add(characterLimb); }
2056  }
2057  }
2058  }
2059  if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb != null)
2060  {
2061  targets.Add(limb);
2062  }
2063 
2064  if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.AddRange(Container.AllPropertyObjects); }
2065 
2066  effect.Apply(type, deltaTime, this, targets, worldPosition);
2067  }
2068 
2069 
2070  public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime,bool playSound = true)
2071  {
2072  if (Indestructible || InvulnerableToDamage) { return new AttackResult(); }
2073 
2074  float damageAmount = attack.GetItemDamage(deltaTime, Prefab.ItemDamageMultiplier);
2075  Condition -= damageAmount;
2076 
2077  if (damageAmount >= Prefab.OnDamagedThreshold)
2078  {
2079  ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
2080  }
2081 
2082  return new AttackResult(damageAmount, null);
2083  }
2084 
2085  private void SetCondition(float value, bool isNetworkEvent, bool executeEffects = true)
2086  {
2087  if (!isNetworkEvent)
2088  {
2089  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
2090  }
2091  if (!MathUtils.IsValid(value)) { return; }
2092  if (Indestructible) { return; }
2093  if (InvulnerableToDamage && value <= condition) { return; }
2094 
2095  bool wasInFullCondition = IsFullCondition;
2096 
2097  float diff = value - condition;
2098  if (GetComponent<Door>() is Door door && door.IsStuck && diff < 0)
2099  {
2100  float dmg = -diff;
2101  // When the door is fully welded shut, reduce the welded state instead of the condition.
2102  float prevStuck = door.Stuck;
2103  door.Stuck -= dmg;
2104  if (door.IsStuck) { return; }
2105  // Reduce the damage by the amount we just adjusted the welded state by.
2106  float damageReduction = dmg - prevStuck;
2107  if (damageReduction < 0) { return; }
2108  value -= damageReduction;
2109  }
2110 
2111  condition = MathHelper.Clamp(value, 0.0f, MaxCondition);
2112 
2113  if (MathUtils.NearlyEqual(prevCondition, value, epsilon: 0.000001f)) { return; }
2114 
2116 
2117  bool wasPreviousConditionChanged = false;
2118  if (condition == 0.0f && prevCondition > 0.0f)
2119  {
2120  //Flag connections to be updated as device is broken
2121  flagChangedConnections(connections);
2122 #if CLIENT
2123  if (executeEffects)
2124  {
2125  foreach (ItemComponent ic in components)
2126  {
2127  ic.PlaySound(ActionType.OnBroken);
2128  ic.StopSounds(ActionType.OnActive);
2129  }
2130  }
2131  if (Screen.Selected == GameMain.SubEditorScreen) { return; }
2132 #endif
2133  // Have to set the previous condition here or OnBroken status effects that reduce the condition will keep triggering the status effects, resulting in a stack overflow.
2134  SetPreviousCondition();
2135  if (executeEffects)
2136  {
2137  ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
2138  }
2139  }
2140  else if (condition > 0.0f && prevCondition <= 0.0f)
2141  {
2142  //Flag connections to be updated as device is now working again
2143  flagChangedConnections(connections);
2144  }
2145 
2146  SetActiveSprite();
2147 
2148  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
2149  {
2150  bool needsConditionUpdate = false;
2151  if (!MathUtils.NearlyEqual(lastSentCondition, condition) && (condition <= 0.0f || condition >= MaxCondition))
2152  {
2153  //send the update immediately if the condition changed to max or min
2154  sendConditionUpdateTimer = 0.0f;
2155  needsConditionUpdate = true;
2156  }
2157  else if (Math.Abs(lastSentCondition - condition) > 1.0f || wasInFullCondition != IsFullCondition)
2158  {
2159  needsConditionUpdate = true;
2160  }
2161  if (needsConditionUpdate && !itemsWithPendingConditionUpdates.Contains(this))
2162  {
2163  itemsWithPendingConditionUpdates.Add(this);
2164  }
2165  }
2166 
2167  if (!wasPreviousConditionChanged)
2168  {
2169  SetPreviousCondition();
2170  }
2171 
2172  void SetPreviousCondition()
2173  {
2174  LastConditionChange = condition - prevCondition;
2175  ConditionLastUpdated = Timing.TotalTime;
2176  prevCondition = condition;
2177  wasPreviousConditionChanged = true;
2178  }
2179 
2180  static void flagChangedConnections(Dictionary<string, Connection> connections)
2181  {
2182  if (connections == null) { return; }
2183  foreach (Connection c in connections.Values)
2184  {
2185  if (c.IsPower)
2186  {
2187  Powered.ChangedConnections.Add(c);
2188  foreach (Connection conn in c.Recipients)
2189  {
2190  Powered.ChangedConnections.Add(conn);
2191  }
2192  }
2193  }
2194  }
2195  }
2196 
2202  {
2203  MaxCondition = Prefab.Health * healthMultiplier * conditionMultiplierCampaign * maxRepairConditionMultiplier * (1.0f + GetQualityModifier(Items.Components.Quality.StatType.Condition));
2204  IsFullCondition = MathUtils.NearlyEqual(Condition, MaxCondition);
2205  ConditionPercentage = MathUtils.Percentage(Condition, MaxCondition);
2206  }
2207 
2208  private bool IsInWater()
2209  {
2210  if (CurrentHull == null) { return true; }
2211 
2212  float surfaceY = CurrentHull.Surface;
2213  return CurrentHull.WaterVolume > 0.0f && Position.Y < surfaceY;
2214  }
2215 
2217  {
2218  if (!(GameMain.NetworkMember is { IsServer: true })) { return; }
2219  if (!itemsWithPendingConditionUpdates.Contains(this)) { return; }
2220  SendPendingNetworkUpdatesInternal();
2221  itemsWithPendingConditionUpdates.Remove(this);
2222  }
2223 
2224  private void SendPendingNetworkUpdatesInternal()
2225  {
2226  CreateStatusEvent(loadingRound: false);
2227  lastSentCondition = condition;
2228  sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
2229  }
2230 
2231  public void CreateStatusEvent(bool loadingRound)
2232  {
2233  GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound));
2234  }
2235 
2236  public static void UpdatePendingConditionUpdates(float deltaTime)
2237  {
2238  if (GameMain.NetworkMember is not { IsServer: true }) { return; }
2239  for (int i = 0; i < itemsWithPendingConditionUpdates.Count; i++)
2240  {
2241  var item = itemsWithPendingConditionUpdates[i];
2242  if (item == null || item.Removed)
2243  {
2244  itemsWithPendingConditionUpdates.RemoveAt(i--);
2245  continue;
2246  }
2247  if (item.Submarine is { Loading: true }) { continue; }
2248 
2249  item.sendConditionUpdateTimer -= deltaTime;
2250  if (item.sendConditionUpdateTimer <= 0.0f)
2251  {
2252  item.SendPendingNetworkUpdatesInternal();
2253  itemsWithPendingConditionUpdates.RemoveAt(i--);
2254  }
2255  }
2256  }
2257 
2258  private bool isActive = true;
2259 
2260  public override void Update(float deltaTime, Camera cam)
2261  {
2262  if (!isActive || IsLayerHidden) { return; }
2263 
2264  if (impactQueue != null)
2265  {
2266  while (impactQueue.TryDequeue(out float impact))
2267  {
2268  HandleCollision(impact);
2269  }
2270  }
2271  if (isDroppedStackOwner && body != null)
2272  {
2273  foreach (var item in droppedStack)
2274  {
2275  if (item != this)
2276  {
2277  item.body.Enabled = false;
2278  item.body.SetTransformIgnoreContacts(this.SimPosition, body.Rotation);
2279  }
2280  }
2281  }
2282 
2283  if (aiTarget != null && aiTarget.NeedsUpdate)
2284  {
2285  aiTarget.Update(deltaTime);
2286  }
2287 
2288  var containedEffectType = parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained;
2289 
2290  ApplyStatusEffects(ActionType.Always, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
2291  ApplyStatusEffects(containedEffectType, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
2292 
2293  for (int i = 0; i < updateableComponents.Count; i++)
2294  {
2295  ItemComponent ic = updateableComponents[i];
2296 
2297  bool isParentInActive = ic.InheritParentIsActive && ic.Parent is { IsActive: false };
2298 
2299  if (ic.IsActiveConditionals != null && !isParentInActive)
2300  {
2302  {
2303  bool shouldBeActive = true;
2304  foreach (var conditional in ic.IsActiveConditionals)
2305  {
2306  if (!ConditionalMatches(conditional))
2307  {
2308  shouldBeActive = false;
2309  break;
2310  }
2311  }
2312  ic.IsActive = shouldBeActive;
2313  }
2314  else
2315  {
2316  bool shouldBeActive = false;
2317  foreach (var conditional in ic.IsActiveConditionals)
2318  {
2319  if (ConditionalMatches(conditional))
2320  {
2321  shouldBeActive = true;
2322  break;
2323  }
2324  }
2325  ic.IsActive = shouldBeActive;
2326  }
2327  }
2328 #if CLIENT
2329  if (ic.HasSounds)
2330  {
2331  ic.PlaySound(ActionType.Always);
2332  ic.UpdateSounds();
2333  if (!ic.WasUsed) { ic.StopSounds(ActionType.OnUse); }
2334  if (!ic.WasSecondaryUsed) { ic.StopSounds(ActionType.OnSecondaryUse); }
2335  }
2336 #endif
2337  ic.WasUsed = false;
2338  ic.WasSecondaryUsed = false;
2339 
2340  if (ic.IsActive || ic.UpdateWhenInactive)
2341  {
2342  if (condition <= 0.0f)
2343  {
2344  ic.UpdateBroken(deltaTime, cam);
2345  }
2346  else
2347  {
2348  ic.Update(deltaTime, cam);
2349 #if CLIENT
2350  if (ic.IsActive)
2351  {
2352  if (ic.IsActiveTimer > 0.02f)
2353  {
2354  ic.PlaySound(ActionType.OnActive);
2355  }
2356  ic.IsActiveTimer += deltaTime;
2357  }
2358 #endif
2359  }
2360  }
2361  }
2362 
2363  if (Removed) { return; }
2364 
2365  bool needsWaterCheck = hasInWaterStatusEffects || hasNotInWaterStatusEffects;
2366  if (body != null && body.Enabled)
2367  {
2368  System.Diagnostics.Debug.Assert(body.FarseerBody.FixtureList != null);
2369 
2370  if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f || transformDirty)
2371  {
2372  if (body.CollisionCategories != Category.None)
2373  {
2374  UpdateTransform();
2375  }
2376  if (CurrentHull == null && Level.Loaded != null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth))
2377  {
2379  return;
2380  }
2381  }
2382  needsWaterCheck = true;
2383  UpdateNetPosition(deltaTime);
2384  if (inWater)
2385  {
2386  ApplyWaterForces();
2387  CurrentHull?.ApplyFlowForces(deltaTime, this);
2388  }
2389  }
2390 
2391  if (needsWaterCheck)
2392  {
2393  bool wasInWater = inWater;
2394  inWater = !inWaterProofContainer && IsInWater();
2395  if (inWater)
2396  {
2397  //the item has gone through the surface of the water
2398  if (!wasInWater && CurrentHull != null && body != null && body.LinearVelocity.Y < -1.0f)
2399  {
2400  Splash();
2401  if (GetComponent<Projectile>() is not { IsActive: true })
2402  {
2403  //slow the item down (not physically accurate, but looks good enough)
2404  body.LinearVelocity *= 0.2f;
2405  }
2406  }
2407  }
2408  if ((hasInWaterStatusEffects || hasNotInWaterStatusEffects) && condition > 0.0f)
2409  {
2410  ApplyStatusEffects(inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
2411  }
2412  if (inWaterProofContainer && !hasNotInWaterStatusEffects)
2413  {
2414  needsWaterCheck = false;
2415  }
2416  }
2417 
2418  if (!needsWaterCheck &&
2419  updateableComponents.Count == 0 &&
2420  (aiTarget == null || !aiTarget.NeedsUpdate) &&
2421  !hasStatusEffectsOfType[(int)ActionType.Always] &&
2422  !hasStatusEffectsOfType[(int)containedEffectType] &&
2423  (body == null || !body.Enabled))
2424  {
2425 #if CLIENT
2426  positionBuffer.Clear();
2427 #endif
2428  isActive = false;
2429  }
2430 
2431  }
2432 
2433  partial void Splash();
2434 
2435  public void UpdateTransform()
2436  {
2437  if (body == null) { return; }
2438  Submarine prevSub = Submarine;
2439 
2440  var projectile = GetComponent<Projectile>();
2441  if (projectile?.StickTarget != null)
2442  {
2443  if (projectile?.StickTarget.UserData is Limb limb && limb.character != null)
2444  {
2445  Submarine = body.Submarine = limb.character.Submarine;
2446  currentHull = limb.character.CurrentHull;
2447  }
2448  else if (projectile.StickTarget.UserData is Structure structure)
2449  {
2450  Submarine = body.Submarine = structure.Submarine;
2451  currentHull = Hull.FindHull(WorldPosition, CurrentHull);
2452  }
2453  else if (projectile.StickTarget.UserData is Item targetItem)
2454  {
2455  Submarine = body.Submarine = targetItem.Submarine;
2456  currentHull = targetItem.CurrentHull;
2457  }
2458  else if (projectile.StickTarget.UserData is Submarine)
2459  {
2460  //attached to a sub from the outside -> don't move inside the sub
2461  Submarine = body.Submarine = null;
2462  currentHull = null;
2463  }
2464  }
2465  else
2466  {
2467  FindHull();
2468  }
2469 
2470  if (Submarine == null && prevSub != null)
2471  {
2473  }
2474  else if (Submarine != null && prevSub == null)
2475  {
2477  }
2478  else if (Submarine != null && prevSub != null && Submarine != prevSub)
2479  {
2481  }
2482 
2483  if (Submarine != prevSub)
2484  {
2485  foreach (Item containedItem in ContainedItems)
2486  {
2487  if (containedItem == null) { continue; }
2488  containedItem.Submarine = Submarine;
2489  }
2490  }
2491 
2492  Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition);
2493  rect.X = (int)(displayPos.X - rect.Width / 2.0f);
2494  rect.Y = (int)(displayPos.Y + rect.Height / 2.0f);
2495 
2496  if (Math.Abs(body.LinearVelocity.X) > NetConfig.MaxPhysicsBodyVelocity ||
2497  Math.Abs(body.LinearVelocity.Y) > NetConfig.MaxPhysicsBodyVelocity)
2498  {
2499  body.LinearVelocity = new Vector2(
2500  MathHelper.Clamp(body.LinearVelocity.X, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity),
2501  MathHelper.Clamp(body.LinearVelocity.Y, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity));
2502  }
2503 
2504  transformDirty = false;
2505  }
2506 
2510  private void ApplyWaterForces()
2511  {
2512  if (body.Mass <= 0.0f || body.Density <= 0.0f)
2513  {
2514  return;
2515  }
2516 
2517  float forceFactor = 1.0f;
2518  if (CurrentHull != null)
2519  {
2520  float floor = CurrentHull.Rect.Y - CurrentHull.Rect.Height;
2521  float waterLevel = floor + CurrentHull.WaterVolume / CurrentHull.Rect.Width;
2522  //forceFactor is 1.0f if the item is completely submerged,
2523  //and goes to 0.0f as the item goes through the surface
2524  forceFactor = Math.Min((waterLevel - Position.Y) / rect.Height, 1.0f);
2525  if (forceFactor <= 0.0f) { return; }
2526  }
2527 
2528  bool moving = body.LinearVelocity.LengthSquared() > 0.001f;
2529  float volume = body.Mass / body.Density;
2530  if (moving)
2531  {
2532  //measure velocity from the velocity of the front of the item and apply the drag to the other end to get the drag to turn the item the "pointy end first"
2533 
2534  //a more "proper" (but more expensive) way to do this would be to e.g. calculate the drag separately for each edge of the fixture
2535  //but since we define the "front" as the "pointy end", we can cheat a bit by using that, and actually even make the drag appear more realistic in some cases
2536  //(e.g. a bullet with a rectangular fixture would be just as "aerodynamic" travelling backwards, but with this method we get it to turn the correct way)
2537  Vector2 localFront = body.GetLocalFront();
2538  Vector2 frontVel = body.FarseerBody.GetLinearVelocityFromLocalPoint(localFront);
2539 
2540  float speed = frontVel.Length();
2541  float drag = speed * speed * WaterDragCoefficient * volume * Physics.NeutralDensity;
2542  //very small drag on active projectiles to prevent affecting their trajectories much
2543  if (body.FarseerBody.IsBullet) { drag *= 0.1f; }
2544  Vector2 dragVec = -frontVel / speed * drag;
2545 
2546  //apply the force slightly towards the back of the item to make it turn the front first
2547  Vector2 back = body.FarseerBody.GetWorldPoint(-localFront * 0.01f);
2548  body.ApplyForce(dragVec, back);
2549  }
2550 
2551  //no need to apply buoyancy if the item is still and not light enough to float
2552  if (moving || body.Density <= 10.0f)
2553  {
2554  Vector2 buoyancy = -GameMain.World.Gravity * forceFactor * volume * Physics.NeutralDensity;
2555  body.ApplyForce(buoyancy);
2556  }
2557 
2558  //apply simple angular drag
2559  if (Math.Abs(body.AngularVelocity) > 0.0001f)
2560  {
2561  body.ApplyTorque(body.AngularVelocity * volume * -0.1f);
2562  }
2563  }
2564 
2565 
2566  private bool OnCollision(Fixture f1, Fixture f2, Contact contact)
2567  {
2568  if (transformDirty) { return false; }
2569 
2570  var projectile = GetComponent<Projectile>();
2571  if (projectile != null)
2572  {
2573  // Ignore characters so that the impact sound only plays when the item hits a a wall or a door.
2574  // Projectile collisions are handled in Projectile.OnProjectileCollision(), so it should be safe to do this.
2575  if (f2.CollisionCategories == Physics.CollisionCharacter) { return false; }
2576  if (projectile.IgnoredBodies != null && projectile.IgnoredBodies.Contains(f2.Body)) { return false; }
2577  if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; }
2578  }
2579 
2580  if (GameMain.GameSession == null || GameMain.GameSession.RoundDuration > 1.0f)
2581  {
2582  contact.GetWorldManifold(out Vector2 normal, out _);
2583  if (contact.FixtureA.Body == f1.Body) { normal = -normal; }
2584  float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
2585  impactQueue ??= new ConcurrentQueue<float>();
2586  impactQueue.Enqueue(impact);
2587  }
2588 
2589  isActive = true;
2590 
2591  return true;
2592  }
2593 
2594  private void HandleCollision(float impact)
2595  {
2596  OnCollisionProjSpecific(impact);
2597  if (GameMain.NetworkMember is { IsClient: true }) { return; }
2598 
2599  if (ImpactTolerance > 0.0f && Math.Abs(impact) > ImpactTolerance && hasStatusEffectsOfType[(int)ActionType.OnImpact])
2600  {
2601  foreach (StatusEffect effect in statusEffectLists[ActionType.OnImpact])
2602  {
2603  ApplyStatusEffect(effect, ActionType.OnImpact, deltaTime: 1.0f);
2604  }
2605 #if SERVER
2606  GameMain.Server?.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnImpact));
2607 #endif
2608  }
2609 
2610  foreach (Item contained in ContainedItems)
2611  {
2612  if (contained.body != null) { contained.HandleCollision(impact); }
2613  }
2614  }
2615 
2616  partial void OnCollisionProjSpecific(float impact);
2617 
2618  public override void FlipX(bool relativeToSub)
2619  {
2620  //call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
2621  base.FlipX(relativeToSub);
2622 
2623  if (!Prefab.CanFlipX)
2624  {
2625  flippedX = false;
2626  return;
2627  }
2628 
2629  if (Prefab.AllowRotatingInEditor)
2630  {
2631  RotationRad = MathUtils.WrapAnglePi(-RotationRad);
2632  }
2633 #if CLIENT
2634  if (Prefab.CanSpriteFlipX)
2635  {
2636  SpriteEffects ^= SpriteEffects.FlipHorizontally;
2637  }
2638 #endif
2639 
2640  foreach (ItemComponent component in components)
2641  {
2642  component.FlipX(relativeToSub);
2643  }
2645  }
2646 
2647  public override void FlipY(bool relativeToSub)
2648  {
2649  //call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
2650  base.FlipY(relativeToSub);
2651 
2652  if (!Prefab.CanFlipY)
2653  {
2654  flippedY = false;
2655  return;
2656  }
2657 
2658  if (Prefab.AllowRotatingInEditor)
2659  {
2660  RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad);
2661  }
2662 #if CLIENT
2663  if (Prefab.CanSpriteFlipY)
2664  {
2665  SpriteEffects ^= SpriteEffects.FlipVertically;
2666  }
2667 #endif
2668 
2669  foreach (ItemComponent component in components)
2670  {
2671  component.FlipY(relativeToSub);
2672  }
2674  }
2675 
2679  public List<T> GetConnectedComponents<T>(bool recursive = false, bool allowTraversingBackwards = true, Func<Connection, bool> connectionFilter = null) where T : ItemComponent
2680  {
2681  List<T> connectedComponents = new List<T>();
2682 
2683  if (recursive)
2684  {
2685  HashSet<Connection> alreadySearched = new HashSet<Connection>();
2686  GetConnectedComponentsRecursive(alreadySearched, connectedComponents, allowTraversingBackwards: allowTraversingBackwards);
2687  return connectedComponents;
2688  }
2689 
2690  ConnectionPanel connectionPanel = GetComponent<ConnectionPanel>();
2691  if (connectionPanel == null) { return connectedComponents; }
2692 
2693  foreach (Connection c in connectionPanel.Connections)
2694  {
2695  if (connectionFilter != null && !connectionFilter.Invoke(c)) { continue; }
2696  foreach (Connection recipient in c.Recipients)
2697  {
2698  var component = recipient.Item.GetComponent<T>();
2699  if (component != null && !connectedComponents.Contains(component))
2700  {
2701  connectedComponents.Add(component);
2702  }
2703  }
2704  }
2705 
2706  return connectedComponents;
2707  }
2708 
2709  private void GetConnectedComponentsRecursive<T>(HashSet<Connection> alreadySearched, List<T> connectedComponents, bool ignoreInactiveRelays = false, bool allowTraversingBackwards = true) where T : ItemComponent
2710  {
2711  ConnectionPanel connectionPanel = GetComponent<ConnectionPanel>();
2712  if (connectionPanel == null) { return; }
2713 
2714  foreach (Connection c in connectionPanel.Connections)
2715  {
2716  if (alreadySearched.Contains(c)) { continue; }
2717  alreadySearched.Add(c);
2718  GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
2719  }
2720  }
2721 
2725  public List<T> GetConnectedComponentsRecursive<T>(Connection c, bool ignoreInactiveRelays = false, bool allowTraversingBackwards = true) where T : ItemComponent
2726  {
2727  List<T> connectedComponents = new List<T>();
2728  HashSet<Connection> alreadySearched = new HashSet<Connection>();
2729  GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
2730 
2731  return connectedComponents;
2732  }
2733 
2734  public static readonly ImmutableArray<(Identifier Input, Identifier Output)> connectionPairs = new (Identifier, Identifier)[]
2735  {
2736  ("power_in".ToIdentifier(), "power_out".ToIdentifier()),
2737  ("signal_in1".ToIdentifier(), "signal_out1".ToIdentifier()),
2738  ("signal_in2".ToIdentifier(), "signal_out2".ToIdentifier()),
2739  ("signal_in3".ToIdentifier(), "signal_out3".ToIdentifier()),
2740  ("signal_in4".ToIdentifier(), "signal_out4".ToIdentifier()),
2741  ("signal_in".ToIdentifier(), "signal_out".ToIdentifier()),
2742  ("signal_in1".ToIdentifier(), "signal_out".ToIdentifier()),
2743  ("signal_in2".ToIdentifier(), "signal_out".ToIdentifier())
2744  }.ToImmutableArray();
2745 
2746  private void GetConnectedComponentsRecursive<T>(Connection c, HashSet<Connection> alreadySearched, List<T> connectedComponents, bool ignoreInactiveRelays, bool allowTraversingBackwards = true) where T : ItemComponent
2747  {
2748  alreadySearched.Add(c);
2749  static IEnumerable<Connection> GetRecipients(Connection c)
2750  {
2751  foreach (Connection recipient in c.Recipients)
2752  {
2753  yield return recipient;
2754  }
2755  //check circuit box inputs/outputs this connection is connected to
2756  foreach (var circuitBoxConnection in c.CircuitBoxConnections)
2757  {
2758  yield return circuitBoxConnection.Connection;
2759  }
2760  }
2761 
2762  foreach (Connection recipient in GetRecipients(c))
2763  {
2764  if (alreadySearched.Contains(recipient)) { continue; }
2765  var component = recipient.Item.GetComponent<T>();
2766  if (component != null && !connectedComponents.Contains(component))
2767  {
2768  connectedComponents.Add(component);
2769  }
2770 
2771  var circuitBox = recipient.Item.GetComponent<CircuitBox>();
2772  if (circuitBox != null)
2773  {
2774  //if this is a circuit box, check what the connection is connected to inside the box
2775  var potentialCbConnection = circuitBox.FindInputOutputConnection(recipient);
2776  if (potentialCbConnection.TryUnwrap(out var cbConnection))
2777  {
2778  if (cbConnection is CircuitBoxInputConnection inputConnection)
2779  {
2780  foreach (var connectedTo in inputConnection.ExternallyConnectedTo)
2781  {
2782  if (alreadySearched.Contains(connectedTo.Connection)) { continue; }
2783  CheckRecipient(connectedTo.Connection);
2784  }
2785  }
2786  else
2787  {
2788  foreach (var connectedFrom in cbConnection.ExternallyConnectedFrom)
2789  {
2790  if (alreadySearched.Contains(connectedFrom.Connection) || !allowTraversingBackwards) { continue; }
2791  CheckRecipient(connectedFrom.Connection);
2792  }
2793  }
2794  }
2795  }
2796  CheckRecipient(recipient);
2797 
2798  void CheckRecipient(Connection recipient)
2799  {
2800  //connected to a wifi component -> see which other wifi components it can communicate with
2801  var wifiComponent = recipient.Item.GetComponent<WifiComponent>();
2802  if (wifiComponent != null && wifiComponent.CanTransmit())
2803  {
2804  foreach (var wifiReceiver in wifiComponent.GetTransmittersInRange())
2805  {
2806  var receiverConnections = wifiReceiver.Item.Connections;
2807  if (receiverConnections == null) { continue; }
2808  foreach (Connection wifiOutput in receiverConnections)
2809  {
2810  if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; }
2811  GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
2812  }
2813  }
2814  }
2815 
2816  recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
2817  }
2818  }
2819 
2820  if (ignoreInactiveRelays)
2821  {
2822  var relay = GetComponent<RelayComponent>();
2823  if (relay != null && !relay.IsOn) { return; }
2824  }
2825 
2826  foreach ((Identifier input, Identifier output) in connectionPairs)
2827  {
2828  void searchFromAToB(Identifier connectionEndA, Identifier connectionEndB)
2829  {
2830  if (connectionEndA == c.Name)
2831  {
2832  var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == connectionEndB);
2833  if (pairedConnection != null)
2834  {
2835  if (alreadySearched.Contains(pairedConnection)) { return; }
2836  GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
2837  }
2838  }
2839  }
2840  searchFromAToB(input, output);
2841  if (allowTraversingBackwards) { searchFromAToB(output, input); }
2842  }
2843  }
2844 
2845  public Controller FindController(ImmutableArray<Identifier>? tags = null)
2846  {
2847  //try finding the controller with the simpler non-recursive method first
2848  var controllers = GetConnectedComponents<Controller>();
2849  bool needsTag = tags != null && tags.Value.Length > 0;
2850  if (controllers.None() || (needsTag && controllers.None(c => c.Item.HasTag(tags))))
2851  {
2852  controllers = GetConnectedComponents<Controller>(recursive: true);
2853  }
2854  if (needsTag)
2855  {
2856  controllers.RemoveAll(c => !c.Item.HasTag(tags));
2857  }
2858  return controllers.Count < 2 ?
2859  controllers.FirstOrDefault() :
2860  controllers.FirstOrDefault(c => c.GetFocusTarget() == this) ?? controllers.FirstOrDefault();
2861  }
2862 
2863  public bool TryFindController(out Controller controller, ImmutableArray<Identifier>? tags = null)
2864  {
2865  controller = FindController(tags: tags);
2866  return controller != null;
2867  }
2868 
2869  public void SendSignal(string signal, string connectionName)
2870  {
2871  SendSignal(new Signal(signal), connectionName);
2872  }
2873 
2874  public void SendSignal(Signal signal, string connectionName)
2875  {
2876  if (connections == null) { return; }
2877  if (!connections.TryGetValue(connectionName, out Connection connection)) { return; }
2878 
2879  signal.source ??= this;
2880  SendSignal(signal, connection);
2881  }
2882 
2883  private readonly HashSet<(Signal Signal, Connection Connection)> delayedSignals = new HashSet<(Signal Signal, Connection Connection)>();
2884 
2885  public void SendSignal(Signal signal, Connection connection)
2886  {
2887  LastSentSignalRecipients.Clear();
2888  if (connections == null || connection == null) { return; }
2889 
2890  signal.stepsTaken++;
2891 
2892  //if the signal has been passed through this item multiple times already, interrupt it to prevent infinite loops
2893  if (signal.stepsTaken > 5 && signal.source != null)
2894  {
2895  int duplicateRecipients = 0;
2896  foreach (var recipient in signal.source.LastSentSignalRecipients)
2897  {
2898  if (recipient == connection)
2899  {
2900  duplicateRecipients++;
2901  if (duplicateRecipients > 2) { return; }
2902  }
2903  }
2904  }
2905 
2906  //use a coroutine to prevent infinite loops by creating a one
2907  //frame delay if the "signal chain" gets too long
2908  if (signal.stepsTaken > 10)
2909  {
2910  //if there's an equal signal waiting to be sent
2911  //to the same connection, don't add a new one
2912  signal.stepsTaken = 0;
2913  bool duplicateFound = false;
2914  foreach (var s in delayedSignals)
2915  {
2916  if (s.Connection == connection && s.Signal.source == signal.source && s.Signal.value == signal.value && s.Signal.sender == signal.sender)
2917  {
2918  duplicateFound = true;
2919  break;
2920  }
2921  }
2922  if (!duplicateFound)
2923  {
2924  delayedSignals.Add((signal, connection));
2925  CoroutineManager.StartCoroutine(DelaySignal(signal, connection));
2926  }
2927  }
2928  else
2929  {
2930  if (connection.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value))
2931  {
2932  foreach (StatusEffect effect in connection.Effects)
2933  {
2934  if (condition <= 0.0f && effect.type != ActionType.OnBroken) { continue; }
2935  ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
2936  }
2937  }
2938  signal.source ??= this;
2939  connection.SendSignal(signal);
2940  }
2941  }
2942 
2943  private IEnumerable<CoroutineStatus> DelaySignal(Signal signal, Connection connection)
2944  {
2945  do
2946  {
2947  //wait at least one frame
2948  yield return CoroutineStatus.Running;
2949  } while (CoroutineManager.DeltaTime <= 0.0f);
2950 
2951  delayedSignals.Remove((signal, connection));
2952  connection.SendSignal(signal);
2953 
2954  yield return CoroutineStatus.Success;
2955  }
2956 
2957  public bool IsInsideTrigger(Vector2 worldPosition)
2958  {
2959  return IsInsideTrigger(worldPosition, out _);
2960  }
2961 
2962  public bool IsInsideTrigger(Vector2 worldPosition, out Rectangle transformedTrigger)
2963  {
2964  foreach (Rectangle trigger in Prefab.Triggers)
2965  {
2966  transformedTrigger = TransformTrigger(trigger, true);
2967  if (Submarine.RectContains(transformedTrigger, worldPosition)) { return true; }
2968  }
2969 
2970  transformedTrigger = Rectangle.Empty;
2971  return false;
2972  }
2973 
2974  public bool CanClientAccess(Client c)
2975  {
2976  return c != null && c.Character != null && c.Character.CanInteractWith(this);
2977  }
2978 
2979  public bool TryInteract(Character user, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceUseKey = false)
2980  {
2981  var campaignInteractionType = CampaignInteractionType;
2982 #if SERVER
2983  var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == user);
2984  if (ownerClient != null)
2985  {
2986  if (!campaignInteractionTypePerClient.TryGetValue(ownerClient, out campaignInteractionType))
2987  {
2988  campaignInteractionType = CampaignMode.InteractionType.None;
2989  }
2990  }
2991 #endif
2992  if (CampaignMode.BlocksInteraction(campaignInteractionType))
2993  {
2994  return false;
2995  }
2996 
2997  bool picked = false, selected = false;
2998 #if CLIENT
2999  bool hasRequiredSkills = true;
3000  Skill requiredSkill = null;
3001  float skillMultiplier = 1;
3002 #endif
3003  if (!IsInteractable(user)) { return false; }
3004  foreach (ItemComponent ic in components)
3005  {
3006  bool pickHit = false, selectHit = false;
3007  if (user.IsKeyDown(InputType.Aim))
3008  {
3009  pickHit = false;
3010  selectHit = false;
3011  }
3012  else
3013  {
3014  if (forceSelectKey)
3015  {
3016  if (ic.PickKey == InputType.Select)
3017  {
3018  pickHit = true;
3019  }
3020  if (ic.SelectKey == InputType.Select)
3021  {
3022  selectHit = true;
3023  }
3024  }
3025  else if (forceUseKey)
3026  {
3027  if (ic.PickKey == InputType.Use)
3028  {
3029  pickHit = true;
3030  }
3031  if (ic.SelectKey == InputType.Use)
3032  {
3033  selectHit = true;
3034  }
3035  }
3036  else
3037  {
3038  pickHit = user.IsKeyHit(ic.PickKey);
3039  selectHit = user.IsKeyHit(ic.SelectKey);
3040 
3041 #if CLIENT
3042  //if the cursor is on a UI component, disable interaction with the left mouse button
3043  //to prevent accidentally selecting items when clicking UI elements
3044  if (user == Character.Controlled && GUI.MouseOn != null)
3045  {
3046  if (GameSettings.CurrentConfig.KeyMap.Bindings[ic.PickKey].MouseButton == 0)
3047  {
3048  pickHit = false;
3049  }
3050 
3051  if (GameSettings.CurrentConfig.KeyMap.Bindings[ic.SelectKey].MouseButton == 0)
3052  {
3053  selectHit = false;
3054  }
3055  }
3056 #endif
3057  }
3058  }
3059 #if CLIENT
3060  //use the non-mouse interaction key (E on both default and legacy keybinds) in wiring mode
3061  //LMB is used to manipulate wires, so using E to select connection panels is much easier
3063  {
3064  pickHit = selectHit = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use].MouseButton == MouseButton.None ?
3065  user.IsKeyHit(InputType.Use) :
3066  user.IsKeyHit(InputType.Select);
3067  }
3068 #endif
3069  if (!pickHit && !selectHit) { continue; }
3070 
3071  bool showUiMsg = false;
3072 #if CLIENT
3073  if (!ic.HasRequiredSkills(user, out Skill tempRequiredSkill)) { hasRequiredSkills = false; skillMultiplier = ic.GetSkillMultiplier(); }
3074  showUiMsg = user == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen;
3075 #endif
3076  if (!ignoreRequiredItems && !ic.HasRequiredItems(user, showUiMsg)) { continue; }
3077  if ((ic.CanBePicked && pickHit && ic.Pick(user)) ||
3078  (ic.CanBeSelected && selectHit && ic.Select(user)))
3079  {
3080  picked = true;
3081  ic.ApplyStatusEffects(ActionType.OnPicked, 1.0f, user);
3082 #if CLIENT
3083  if (user == Character.Controlled) { GUI.ForceMouseOn(null); }
3084  if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; }
3085 #endif
3086  if (ic.CanBeSelected && ic is not Door) { selected = true; }
3087  }
3088  }
3089  if (ParentInventory?.Owner == user &&
3090  GetComponent<ItemContainer>() != null)
3091  {
3092  //can't select ItemContainers in the character's inventory
3093  //(the inventory is drawn by hovering the cursor over the inventory slot, not as a hovering interface on the screen)
3094  selected = false;
3095  }
3096 
3097  if (!picked) { return false; }
3098 
3099  OnInteract?.Invoke();
3100 
3101  if (user != null)
3102  {
3103  if (user.SelectedItem == this)
3104  {
3105  if (user.IsKeyHit(InputType.Select) || forceSelectKey)
3106  {
3107  user.SelectedItem = null;
3108  }
3109  }
3110  else if (user.SelectedSecondaryItem == this)
3111  {
3112  if (user.IsKeyHit(InputType.Select) || forceSelectKey)
3113  {
3114  user.SelectedSecondaryItem = null;
3115  }
3116  }
3117  else if (selected)
3118  {
3119  if (IsSecondaryItem)
3120  {
3121  user.SelectedSecondaryItem = this;
3122  }
3123  else
3124  {
3125  user.SelectedItem = this;
3126  }
3127  }
3128  }
3129 
3130 #if CLIENT
3131  if (!hasRequiredSkills && Character.Controlled == user && Screen.Selected != GameMain.SubEditorScreen)
3132  {
3133  if (requiredSkill != null)
3134  {
3135  GUI.AddMessage(TextManager.GetWithVariables("InsufficientSkills",
3136  ("[requiredskill]", TextManager.Get("SkillName." + requiredSkill.Identifier), FormatCapitals.Yes),
3137  ("[requiredlevel]", ((int)(requiredSkill.Level * skillMultiplier)).ToString(), FormatCapitals.No)), GUIStyle.Red);
3138  }
3139  }
3140 #endif
3141 
3142  if (Container != null)
3143  {
3144  Container.RemoveContained(this);
3145  }
3146 
3147  return true;
3148  }
3149 
3151  {
3152  if (ownInventory == null) { return -1; }
3153 
3154  float condition = 0f;
3155  float maxCondition = 0f;
3156  foreach (Item item in ContainedItems)
3157  {
3158  condition += item.condition;
3159  maxCondition += item.MaxCondition;
3160  }
3161  if (maxCondition > 0.0f)
3162  {
3163  return condition / maxCondition;
3164  }
3165 
3166  return -1;
3167  }
3168 
3172  public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null, Character userForOnUsedEvent = null)
3173  {
3174  if (RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim)))
3175  {
3176  return;
3177  }
3178 
3179  if (condition <= 0.0f) { return; }
3180 
3181  var should = GameMain.LuaCs.Hook.Call<bool?>("item.use", new object[] { this, user, targetLimb, useTarget });
3182 
3183  if (should != null && should.Value) { return; }
3184 
3185  bool remove = false;
3186 
3187  foreach (ItemComponent ic in components)
3188  {
3189  bool isControlled = false;
3190 #if CLIENT
3191  isControlled = user == Character.Controlled;
3192 #endif
3193  if (!ic.HasRequiredContainedItems(user, isControlled)) { continue; }
3194  if (ic.Use(deltaTime, user))
3195  {
3196  ic.WasUsed = true;
3197 #if CLIENT
3198  ic.PlaySound(ActionType.OnUse, user);
3199 #endif
3200  ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, user, targetLimb, useTarget: useTarget, user: user);
3201  ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user ?? userForOnUsedEvent));
3202  if (ic.DeleteOnUse) { remove = true; }
3203  }
3204  }
3205 
3206  if (remove)
3207  {
3209  }
3210  }
3211 
3212  public void SecondaryUse(float deltaTime, Character character = null)
3213  {
3214  if (condition <= 0.0f) { return; }
3215 
3216  var should = GameMain.LuaCs.Hook.Call<bool?>("item.secondaryUse", this, character);
3217 
3218  if (should != null && should.Value)
3219  return;
3220 
3221  bool remove = false;
3222 
3223  foreach (ItemComponent ic in components)
3224  {
3225  bool isControlled = false;
3226 #if CLIENT
3227  isControlled = character == Character.Controlled;
3228 #endif
3229  if (!ic.HasRequiredContainedItems(character, isControlled)) { continue; }
3230  if (ic.SecondaryUse(deltaTime, character))
3231  {
3232  ic.WasSecondaryUsed = true;
3233 #if CLIENT
3234  ic.PlaySound(ActionType.OnSecondaryUse, character);
3235 #endif
3236  ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character, useTarget: character);
3237 
3238  if (ic.DeleteOnUse) { remove = true; }
3239  }
3240  }
3241 
3242  if (remove)
3243  {
3245  }
3246  }
3247 
3248  public void ApplyTreatment(Character user, Character character, Limb targetLimb)
3249  {
3250  //can't apply treatment to dead characters
3251  if (character.IsDead) { return; }
3252  if (!UseInHealthInterface) { return; }
3253 
3254  if (Prefab.ContentPackage == ContentPackageManager.VanillaCorePackage &&
3255  /* we don't need info of every item, we can get a good sample size just by logging 5% */
3256  Rand.Range(0.0f, 1.0f) < 0.05f)
3257  {
3258  GameAnalyticsManager.AddDesignEvent("ApplyTreatment:" + Prefab.Identifier);
3259  }
3260 #if CLIENT
3261  if (user == Character.Controlled)
3262  {
3263  if (HealingCooldown.IsOnCooldown) { return; }
3264 
3265  HealingCooldown.PutOnCooldown();
3266  }
3267 
3268  if (GameMain.Client != null)
3269  {
3270  GameMain.Client.CreateEntityEvent(this, new TreatmentEventData(character, targetLimb));
3271  return;
3272  }
3273 #endif
3274  bool remove = false;
3275  foreach (ItemComponent ic in components)
3276  {
3277  if (!ic.HasRequiredContainedItems(user, addMessage: user == Character.Controlled)) { continue; }
3278 
3279  bool success = Rand.Range(0.0f, 0.5f) < ic.DegreeOfSuccess(user);
3280  ActionType conditionalActionType = success ? ActionType.OnSuccess : ActionType.OnFailure;
3281 
3282 #if CLIENT
3283  ic.PlaySound(conditionalActionType, user);
3284  ic.PlaySound(ActionType.OnUse, user);
3285 #endif
3286  ic.WasUsed = true;
3287 
3288  ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: user);
3289  ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: character, user: user);
3290 
3291  if (GameMain.NetworkMember is { IsServer: true })
3292  {
3293  GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb, useTarget: character));
3294  GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb, useTarget: character));
3295  }
3296 
3297  if (ic.DeleteOnUse) { remove = true; }
3298  }
3299 
3300  if (user != null)
3301  {
3302  var abilityItem = new AbilityApplyTreatment(user, character, this, targetLimb);
3303  user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem);
3304  }
3305 
3306  if (remove) { Spawner?.AddItemToRemoveQueue(this); }
3307  }
3308 
3309  public bool Combine(Item item, Character user)
3310  {
3311  if (item == this) { return false; }
3312  bool isCombined = false;
3313  foreach (ItemComponent ic in components)
3314  {
3315  if (ic.Combine(item, user)) { isCombined = true; }
3316  }
3317 #if CLIENT
3318  if (isCombined) { GameMain.Client?.CreateEntityEvent(this, new CombineEventData(item)); }
3319 #endif
3320  return isCombined;
3321  }
3322 
3329  public void Drop(Character dropper, bool createNetworkEvent = true, bool setTransform = true)
3330  {
3331  if (createNetworkEvent)
3332  {
3333  if (parentInventory != null && !parentInventory.Owner.Removed && !Removed &&
3334  GameMain.NetworkMember != null && (GameMain.NetworkMember.IsServer || Character.Controlled == dropper))
3335  {
3336  parentInventory.CreateNetworkEvent();
3337  //send frequent updates after the item has been dropped
3338  PositionUpdateInterval = 0.0f;
3339  }
3340  }
3341 
3342  if (body != null)
3343  {
3344  isActive = true;
3345  body.Enabled = true;
3346  body.PhysEnabled = true;
3347  body.ResetDynamics();
3348  if (dropper != null)
3349  {
3350  if (body.Removed)
3351  {
3352  DebugConsole.ThrowError(
3353  "Failed to drop the item \"" + Name + "\" (body has been removed"
3354  + (Removed ? ", item has been removed)" : ")"));
3355  }
3356  else if (setTransform)
3357  {
3358  body.SetTransform(dropper.SimPosition, 0.0f);
3359  }
3360  }
3361  }
3362 
3363  foreach (ItemComponent ic in components) { ic.Drop(dropper, setTransform); }
3364 
3365  if (Container != null)
3366  {
3367  if (setTransform)
3368  {
3370  }
3371  Container.RemoveContained(this);
3372  Container = null;
3373  }
3374 
3375  if (ParentInventory != null)
3376  {
3378  ParentInventory = null;
3379  }
3380 
3382 #if CLIENT
3384 #endif
3385  }
3386 
3387 
3388  private List<Item> droppedStack;
3389  public IEnumerable<Item> DroppedStack => droppedStack ?? Enumerable.Empty<Item>();
3390 
3391  private bool isDroppedStackOwner;
3392 
3397  public void CreateDroppedStack(IEnumerable<Item> items, bool allowClientExecute)
3398  {
3399  if (!allowClientExecute && GameMain.NetworkMember is { IsClient: true }) { return; }
3400 
3401  int itemCount = items.Count();
3402 
3403  if (itemCount == 1) { return; }
3404 
3405  if (items.DistinctBy(it => it.Prefab).Count() > 1)
3406  {
3407  DebugConsole.ThrowError($"Attempted to create a dropped stack of multiple different items ({string.Join(", ", items.DistinctBy(it => it.Prefab))})\n{Environment.StackTrace}");
3408  return;
3409  }
3410  if (items.Any(it => it.body == null))
3411  {
3412  DebugConsole.ThrowError($"Attempted to create a dropped stack for an item with no body ({items.First().Prefab.Identifier})\n{Environment.StackTrace}");
3413  return;
3414  }
3415  if (items.None())
3416  {
3417  DebugConsole.ThrowError($"Attempted to create a dropped stack of an empty list of items.\n{Environment.StackTrace}");
3418  return;
3419  }
3420 
3421  int maxStackSize = items.First().Prefab.MaxStackSize;
3422  if (itemCount > maxStackSize)
3423  {
3424  for (int i = 0; i < MathF.Ceiling(itemCount / maxStackSize); i++)
3425  {
3426  int startIndex = i * maxStackSize;
3427  items.ElementAt(startIndex).CreateDroppedStack(items.Skip(startIndex).Take(maxStackSize), allowClientExecute);
3428  }
3429  }
3430  else
3431  {
3432  droppedStack ??= new List<Item>();
3433  foreach (Item item in items)
3434  {
3435  if (!droppedStack.Contains(item))
3436  {
3437  droppedStack.Add(item);
3438  }
3439  }
3440  SetDroppedStackItemStates();
3441 #if SERVER
3442  if (GameMain.NetworkMember is { IsServer: true } server)
3443  {
3444  server.CreateEntityEvent(this, new DroppedStackEventData(droppedStack));
3445  }
3446 #endif
3447  }
3448  }
3449 
3450  private void RemoveFromDroppedStack(bool allowClientExecute)
3451  {
3452  if (!allowClientExecute && GameMain.NetworkMember is { IsClient: true }) { return; }
3453  if (droppedStack == null) { return; }
3454 
3455  body.Enabled = ParentInventory == null;
3456  isDroppedStackOwner = false;
3457  droppedStack.Remove(this);
3458  SetDroppedStackItemStates();
3459  droppedStack = null;
3460 #if SERVER
3461  if (GameMain.NetworkMember is { IsServer: true } server && !Removed)
3462  {
3463  server.CreateEntityEvent(this, new DroppedStackEventData(Enumerable.Empty<Item>()));
3464  }
3465 #endif
3466  }
3467 
3468  private void SetDroppedStackItemStates()
3469  {
3470  if (droppedStack == null) { return; }
3471  bool isFirst = true;
3472  foreach (Item item in droppedStack)
3473  {
3474  item.droppedStack = droppedStack;
3475  item.isDroppedStackOwner = isFirst;
3476  if (item.body != null)
3477  {
3478  item.body.Enabled = item.body.PhysEnabled = isFirst;
3479  if (isFirst)
3480  {
3481  item.isActive = true;
3482  item.body.ResetDynamics();
3483  }
3484  }
3485  isFirst = false;
3486  }
3487  }
3488 
3489 
3494  public IEnumerable<Item> GetStackedItems()
3495  {
3496  yield return this;
3497  foreach (var stackedItem in DroppedStack)
3498  {
3499  if (stackedItem == this) { continue; }
3500  yield return stackedItem;
3501  }
3502  if (ParentInventory != null)
3503  {
3504  int slotIndex = ParentInventory.FindIndex(this);
3505  foreach (var stackedItem in ParentInventory.GetItemsAt(slotIndex))
3506  {
3507  if (stackedItem == this) { continue; }
3508  yield return stackedItem;
3509  }
3510  }
3511  }
3512 
3513  public void Equip(Character character)
3514  {
3515  if (Removed)
3516  {
3517  DebugConsole.ThrowError($"Tried to equip a removed item ({Name}).\n{Environment.StackTrace.CleanupStackTrace()}");
3518  return;
3519  }
3520 
3521  foreach (ItemComponent ic in components) { ic.Equip(character); }
3522 
3523  CharacterHUD.RecreateHudTextsIfControlling(character);
3524  }
3525 
3526  public void Unequip(Character character)
3527  {
3528  foreach (ItemComponent ic in components) { ic.Unequip(character); }
3529  CharacterHUD.RecreateHudTextsIfControlling(character);
3530  }
3531 
3532  public List<(object obj, SerializableProperty property)> GetProperties<T>()
3533  {
3534  List<(object obj, SerializableProperty property)> allProperties = new List<(object obj, SerializableProperty property)>();
3535 
3536  List<SerializableProperty> itemProperties = SerializableProperty.GetProperties<T>(this);
3537  foreach (var itemProperty in itemProperties)
3538  {
3539  allProperties.Add((this, itemProperty));
3540  }
3541  foreach (ItemComponent ic in components)
3542  {
3543  List<SerializableProperty> componentProperties = SerializableProperty.GetProperties<T>(ic);
3544  foreach (var componentProperty in componentProperties)
3545  {
3546  allProperties.Add((ic, componentProperty));
3547  }
3548  }
3549  return allProperties;
3550  }
3551 
3552  private void WritePropertyChange(IWriteMessage msg, ChangePropertyEventData extraData, bool inGameEditableOnly)
3553  {
3554  //ignoreConditions: true = include all ConditionallyEditable properties at this point,
3555  //to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients
3556  var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
3557  SerializableProperty property = extraData.SerializableProperty;
3558  ISerializableEntity entity = extraData.Entity;
3559  if (property != null)
3560  {
3561  if (allProperties.Count > 1)
3562  {
3563  int propertyIndex = allProperties.FindIndex(p => p.property == property && p.obj == entity);
3564  if (propertyIndex < 0)
3565  {
3566  throw new Exception($"Could not find the property \"{property.Name}\" in \"{entity.Name ?? "null"}\"");
3567  }
3568  msg.WriteVariableUInt32((uint)propertyIndex);
3569  }
3570 
3571  object value = property.GetValue(entity);
3572  if (value is string stringVal)
3573  {
3574  msg.WriteString(stringVal);
3575  }
3576  else if (value is Identifier idValue)
3577  {
3578  msg.WriteIdentifier(idValue);
3579  }
3580  else if (value is float floatVal)
3581  {
3582  msg.WriteSingle(floatVal);
3583  }
3584  else if (value is int intVal)
3585  {
3586  msg.WriteInt32(intVal);
3587  }
3588  else if (value is bool boolVal)
3589  {
3590  msg.WriteBoolean(boolVal);
3591  }
3592  else if (value is Color color)
3593  {
3594  msg.WriteByte(color.R);
3595  msg.WriteByte(color.G);
3596  msg.WriteByte(color.B);
3597  msg.WriteByte(color.A);
3598  }
3599  else if (value is Vector2 vector2)
3600  {
3601  msg.WriteSingle(vector2.X);
3602  msg.WriteSingle(vector2.Y);
3603  }
3604  else if (value is Vector3 vector3)
3605  {
3606  msg.WriteSingle(vector3.X);
3607  msg.WriteSingle(vector3.Y);
3608  msg.WriteSingle(vector3.Z);
3609  }
3610  else if (value is Vector4 vector4)
3611  {
3612  msg.WriteSingle(vector4.X);
3613  msg.WriteSingle(vector4.Y);
3614  msg.WriteSingle(vector4.Z);
3615  msg.WriteSingle(vector4.W);
3616  }
3617  else if (value is Point point)
3618  {
3619  msg.WriteInt32(point.X);
3620  msg.WriteInt32(point.Y);
3621  }
3622  else if (value is Rectangle rect)
3623  {
3624  msg.WriteInt32(rect.X);
3625  msg.WriteInt32(rect.Y);
3626  msg.WriteInt32(rect.Width);
3627  msg.WriteInt32(rect.Height);
3628  }
3629  else if (value is Enum)
3630  {
3631  msg.WriteInt32((int)value);
3632  }
3633  else if (value is string[] a)
3634  {
3635  msg.WriteInt32(a.Length);
3636  for (int i = 0; i < a.Length; i++)
3637  {
3638  msg.WriteString(a[i] ?? "");
3639  }
3640  }
3641  else
3642  {
3643  throw new NotImplementedException("Serializing item properties of the type \"" + value.GetType() + "\" not supported");
3644  }
3645  }
3646  else
3647  {
3648  throw new ArgumentException("Failed to write propery value - property \"" + (property == null ? "null" : property.Name) + "\" is not serializable.");
3649  }
3650  }
3651 
3652  private List<(object obj, SerializableProperty property)> GetInGameEditableProperties(bool ignoreConditions = false)
3653  {
3654  if (ignoreConditions)
3655  {
3656  return GetProperties<ConditionallyEditable>().Union(GetProperties<InGameEditable>()).ToList();
3657  }
3658  else
3659  {
3660  return GetProperties<ConditionallyEditable>()
3661  .Where(ce => ce.property.GetAttribute<ConditionallyEditable>().IsEditable(this))
3662  .Union(GetProperties<InGameEditable>()).ToList();
3663  }
3664  }
3665 
3666  private void ReadPropertyChange(IReadMessage msg, bool inGameEditableOnly, Client sender = null)
3667  {
3668  //ignoreConditions: true = include all ConditionallyEditable properties at this point,
3669  //to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients
3670  var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
3671  if (allProperties.Count == 0) { return; }
3672 
3673  int propertyIndex = 0;
3674  if (allProperties.Count > 1)
3675  {
3676  propertyIndex = (int)msg.ReadVariableUInt32();
3677  }
3678 
3679  if (propertyIndex >= allProperties.Count || propertyIndex < 0)
3680  {
3681  throw new Exception($"Error in ReadPropertyChange. Property index out of bounds (index: {propertyIndex}, property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})");
3682  }
3683 
3684  bool allowEditing = true;
3685  object parentObject = allProperties[propertyIndex].obj;
3686  SerializableProperty property = allProperties[propertyIndex].property;
3687  if (inGameEditableOnly && parentObject is ItemComponent ic)
3688  {
3689  if (!ic.AllowInGameEditing) { allowEditing = false; }
3690  }
3691 
3692  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
3693  {
3694  bool conditionAllowsEditing = true;
3695  if (property.GetAttribute<ConditionallyEditable>() is { } condition)
3696  {
3697  conditionAllowsEditing = condition.IsEditable(this);
3698  }
3699 
3700  bool canAccess = false;
3701  if (Container?.GetComponent<CircuitBox>() is { } cb &&
3702  Container.CanClientAccess(sender))
3703  {
3704  //items inside circuit boxes are inaccessible by "normal" means,
3705  //but the properties can still be edited through the circuit box UI
3706  canAccess = !cb.Locked;
3707  }
3708  else
3709  {
3710  canAccess = CanClientAccess(sender);
3711  }
3712 
3713  if (!canAccess || !conditionAllowsEditing)
3714  {
3715  allowEditing = false;
3716  }
3717  }
3718 
3719  var result = GameMain.LuaCs.Hook.Call<bool?>("item.readPropertyChange", this, property, parentObject, allowEditing, sender);
3720  if (result != null && result.Value)
3721  return;
3722 
3723  Type type = property.PropertyType;
3724  string logValue = "";
3725  if (type == typeof(string))
3726  {
3727  string val = msg.ReadString();
3728  logValue = val;
3729  if (allowEditing)
3730  {
3731  property.TrySetValue(parentObject, val);
3732  }
3733  }
3734  else if (type == typeof(Identifier))
3735  {
3736  Identifier val = msg.ReadIdentifier();
3737  logValue = val.Value;
3738  if (allowEditing) { property.TrySetValue(parentObject, val); }
3739  }
3740  else if (type == typeof(float))
3741  {
3742  float val = msg.ReadSingle();
3743  logValue = val.ToString("G", CultureInfo.InvariantCulture);
3744  if (allowEditing) { property.TrySetValue(parentObject, val); }
3745  }
3746  else if (type == typeof(int))
3747  {
3748  int val = msg.ReadInt32();
3749  logValue = val.ToString();
3750  if (allowEditing) { property.TrySetValue(parentObject, val); }
3751  }
3752  else if (type == typeof(bool))
3753  {
3754  bool val = msg.ReadBoolean();
3755  logValue = val.ToString();
3756  if (allowEditing) { property.TrySetValue(parentObject, val); }
3757  }
3758  else if (type == typeof(Color))
3759  {
3760  Color val = new Color(msg.ReadByte(), msg.ReadByte(), msg.ReadByte(), msg.ReadByte());
3761  logValue = XMLExtensions.ColorToString(val);
3762  if (allowEditing) { property.TrySetValue(parentObject, val); }
3763  }
3764  else if (type == typeof(Vector2))
3765  {
3766  Vector2 val = new Vector2(msg.ReadSingle(), msg.ReadSingle());
3767  logValue = XMLExtensions.Vector2ToString(val);
3768  if (allowEditing) { property.TrySetValue(parentObject, val); }
3769  }
3770  else if (type == typeof(Vector3))
3771  {
3772  Vector3 val = new Vector3(msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle());
3773  logValue = XMLExtensions.Vector3ToString(val);
3774  if (allowEditing) { property.TrySetValue(parentObject, val); }
3775  }
3776  else if (type == typeof(Vector4))
3777  {
3778  Vector4 val = new Vector4(msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle());
3779  logValue = XMLExtensions.Vector4ToString(val);
3780  if (allowEditing) { property.TrySetValue(parentObject, val); }
3781  }
3782  else if (type == typeof(Point))
3783  {
3784  Point val = new Point(msg.ReadInt32(), msg.ReadInt32());
3785  logValue = XMLExtensions.PointToString(val);
3786  if (allowEditing) { property.TrySetValue(parentObject, val); }
3787  }
3788  else if (type == typeof(Rectangle))
3789  {
3790  Rectangle val = new Rectangle(msg.ReadInt32(), msg.ReadInt32(), msg.ReadInt32(), msg.ReadInt32());
3791  logValue = XMLExtensions.RectToString(val);
3792  if (allowEditing) { property.TrySetValue(parentObject, val); }
3793  }
3794  else if (type == typeof(string[]))
3795  {
3796  int arrayLength = msg.ReadInt32();
3797  string[] val = new string[arrayLength];
3798  for (int i = 0; i < arrayLength; i++)
3799  {
3800  val[i] = msg.ReadString();
3801  }
3802  if (allowEditing)
3803  {
3804  property.TrySetValue(parentObject, val);
3805  }
3806  }
3807  else if (typeof(Enum).IsAssignableFrom(type))
3808  {
3809  int intVal = msg.ReadInt32();
3810  try
3811  {
3812  if (allowEditing)
3813  {
3814  property.TrySetValue(parentObject, Enum.ToObject(type, intVal));
3815  logValue = property.GetValue(parentObject).ToString();
3816  }
3817  }
3818  catch (Exception e)
3819  {
3820 #if DEBUG
3821  DebugConsole.ThrowError("Failed to convert the int value \"" + intVal + "\" to " + type, e);
3822 #endif
3823  GameAnalyticsManager.AddErrorEventOnce(
3824  "Item.ReadPropertyChange:" + Name + ":" + type,
3825  GameAnalyticsManager.ErrorSeverity.Warning,
3826  "Failed to convert the int value \"" + intVal + "\" to " + type + " (item " + Name + ")");
3827  }
3828  }
3829  else
3830  {
3831  return;
3832  }
3833 
3834 #if SERVER
3835  if (allowEditing)
3836  {
3837  //the property change isn't logged until the value stays unchanged for 1 second to prevent log spam when a player adjusts a value
3838  if (logPropertyChangeCoroutine != null)
3839  {
3840  CoroutineManager.StopCoroutines(logPropertyChangeCoroutine);
3841  }
3842  logPropertyChangeCoroutine = CoroutineManager.Invoke(() =>
3843  {
3844  GameServer.Log($"{sender.Character?.Name ?? sender.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction);
3845  }, delay: 1.0f);
3846  }
3847 #endif
3848 
3849  if (GameMain.NetworkMember is { IsServer: true } && parentObject is ISerializableEntity entity)
3850  {
3851  GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property, entity));
3852  }
3853  }
3854 
3855  partial void UpdateNetPosition(float deltaTime);
3856 
3857  public static Item Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
3858  {
3859  return Load(element, submarine, createNetworkEvent: false, idRemap: idRemap);
3860  }
3861 
3869  public static Item Load(ContentXElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap)
3870  {
3871  string name = element.GetAttribute("name").Value;
3872  Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
3873 
3874  if (string.IsNullOrWhiteSpace(name) && identifier.IsEmpty)
3875  {
3876  string errorMessage = "Failed to load an item (both name and identifier were null):\n"+element.ToString();
3877  DebugConsole.ThrowError(errorMessage);
3878  GameAnalyticsManager.AddErrorEventOnce("Item.Load:NameAndIdentifierNull", GameAnalyticsManager.ErrorSeverity.Error, errorMessage);
3879  return null;
3880  }
3881 
3882  Identifier pendingSwap = element.GetAttributeIdentifier("pendingswap", Identifier.Empty);
3883  ItemPrefab appliedSwap = null;
3884  ItemPrefab oldPrefab = null;
3885  if (!pendingSwap.IsEmpty && Level.Loaded?.Type != LevelData.LevelType.Outpost)
3886  {
3887  oldPrefab = ItemPrefab.Find(name, identifier);
3888  appliedSwap = ItemPrefab.Find(string.Empty, pendingSwap);
3889  identifier = pendingSwap;
3890  pendingSwap = Identifier.Empty;
3891  }
3892 
3893  ItemPrefab prefab = ItemPrefab.Find(name, identifier);
3894  if (prefab == null) { return null; }
3895 
3896  Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty);
3897  Vector2 centerPos = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2);
3898  if (appliedSwap != null)
3899  {
3900  rect.Width = (int)(prefab.Sprite.size.X * prefab.Scale);
3901  rect.Height = (int)(prefab.Sprite.size.Y * prefab.Scale);
3902  }
3903  else if (rect.Width == 0 && rect.Height == 0)
3904  {
3905  rect.Width = (int)(prefab.Size.X * prefab.Scale);
3906  rect.Height = (int)(prefab.Size.Y * prefab.Scale);
3907  }
3908 
3909  Item item = new Item(rect, prefab, submarine, callOnItemLoaded: false, id: idRemap.GetOffsetId(element))
3910  {
3911  Submarine = submarine,
3912  linkedToID = new List<ushort>(),
3913  PendingItemSwap = pendingSwap.IsEmpty ? null : MapEntityPrefab.Find(pendingSwap.Value) as ItemPrefab
3914  };
3915 
3916 #if SERVER
3917  if (createNetworkEvent)
3918  {
3919  Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
3920  }
3921 #endif
3922 
3923  foreach (XAttribute attribute in (appliedSwap?.ConfigElement ?? element).Attributes())
3924  {
3925  if (!item.SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; }
3926  bool shouldBeLoaded = false;
3927  foreach (var propertyAttribute in property.Attributes.OfType<Serialize>())
3928  {
3929  if (propertyAttribute.IsSaveable == IsPropertySaveable.Yes)
3930  {
3931  shouldBeLoaded = true;
3932  break;
3933  }
3934  }
3935 
3936  if (shouldBeLoaded)
3937  {
3938  object prevValue = property.GetValue(item);
3939  property.TrySetValue(item, attribute.Value);
3940  //create network events for properties that differ from the prefab values
3941  //(e.g. if a character has an item with modified colors in their inventory)
3942  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && property.Attributes.OfType<Editable>().Any() &&
3943  (submarine == null || !submarine.Loading))
3944  {
3945  if (property.Name == "Tags" ||
3946  property.Name == "Condition" ||
3947  property.Name == "Description")
3948  {
3949  //these can be ignored, they're always written in the spawn data
3950  }
3951  else
3952  {
3953  if (!(property.GetValue(item)?.Equals(prevValue) ?? true))
3954  {
3955  GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property, item));
3956  }
3957  }
3958  }
3959  }
3960  }
3961 
3962  item.ParseLinks(element, idRemap);
3963 
3964  bool thisIsOverride = element.GetAttributeBool("isoverride", false);
3965 
3966  //if we're overriding a non-overridden item in a sub/assembly xml or vice versa,
3967  //use the values from the prefab instead of loading them from the sub/assembly xml
3968  bool isItemSwap = appliedSwap != null;
3969  bool usePrefabValues = thisIsOverride != ItemPrefab.Prefabs.IsOverride(prefab) || isItemSwap;
3970  List<ItemComponent> unloadedComponents = new List<ItemComponent>(item.components);
3971  foreach (var subElement in element.Elements())
3972  {
3973  switch (subElement.Name.ToString().ToLowerInvariant())
3974  {
3975  case "upgrade":
3976  {
3977  var upgradeIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
3978  UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier);
3979  int level = subElement.GetAttributeInt("level", 1);
3980  if (upgradePrefab != null)
3981  {
3982  item.AddUpgrade(new Upgrade(item, upgradePrefab, level, isItemSwap ? null : subElement));
3983  }
3984  else
3985  {
3986  DebugConsole.AddWarning($"An upgrade with identifier \"{upgradeIdentifier}\" on {item.Name} was not found. " +
3987  "It's effect will not be applied and won't be saved after the round ends.");
3988  }
3989  break;
3990  }
3991  case "itemstats":
3992  {
3993  item.StatManager.Load(subElement);
3994  break;
3995  }
3996  default:
3997  {
3998  ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString());
3999  if (component == null) { continue; }
4000  component.Load(subElement, usePrefabValues, idRemap, isItemSwap);
4001  unloadedComponents.Remove(component);
4002  break;
4003  }
4004  }
4005  }
4006  if (usePrefabValues && !isItemSwap)
4007  {
4008  //use prefab scale when overriding a non-overridden item or vice versa
4009  item.Scale = prefab.ConfigElement.GetAttributeFloat(item.scale, "scale", "Scale");
4010  }
4011 
4012  item.Upgrades.ForEach(upgrade => upgrade.ApplyUpgrade());
4013 
4014  var availableSwapIds = element.GetAttributeIdentifierArray("availableswaps", Array.Empty<Identifier>());
4015  foreach (Identifier swapId in availableSwapIds)
4016  {
4017  ItemPrefab swapPrefab = ItemPrefab.Find(string.Empty, swapId);
4018  if (swapPrefab != null)
4019  {
4020  item.AvailableSwaps.Add(swapPrefab);
4021  }
4022  }
4023 
4024  if (element.GetAttributeBool("markedfordeconstruction", false)) { deconstructItems.Add(item); }
4025 
4026  float prevRotation = item.Rotation;
4027  if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); }
4028  if (element.GetAttributeBool("flippedy", false)) { item.FlipY(false); }
4029  item.Rotation = prevRotation;
4030 
4031  if (appliedSwap != null)
4032  {
4033  item.SpriteDepth = element.GetAttributeFloat("spritedepth", item.SpriteDepth);
4034  item.SpriteColor = element.GetAttributeColor("spritecolor", item.SpriteColor);
4035  item.Rotation = element.GetAttributeFloat("rotation", item.Rotation);
4036  item.PurchasedNewSwap = element.GetAttributeBool("purchasednewswap", false);
4037 
4038  float scaleRelativeToPrefab = element.GetAttributeFloat(item.scale, "scale", "Scale") / oldPrefab.Scale;
4039  item.Scale *= scaleRelativeToPrefab;
4040 
4041  if (oldPrefab.SwappableItem != null && prefab.SwappableItem != null)
4042  {
4043  Vector2 oldRelativeOrigin = (oldPrefab.SwappableItem.SwapOrigin - oldPrefab.Size / 2) * element.GetAttributeFloat(item.scale, "scale", "Scale");
4044  oldRelativeOrigin.Y = -oldRelativeOrigin.Y;
4045  oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, -item.RotationRad);
4046  Vector2 oldOrigin = centerPos + oldRelativeOrigin;
4047 
4048  Vector2 relativeOrigin = (prefab.SwappableItem.SwapOrigin - prefab.Size / 2) * item.Scale;
4049  relativeOrigin.Y = -relativeOrigin.Y;
4050  relativeOrigin = MathUtils.RotatePoint(relativeOrigin, -item.RotationRad);
4051  Vector2 origin = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + relativeOrigin;
4052 
4053  item.rect.Location -= (origin - oldOrigin).ToPoint();
4054  }
4055 
4056  if (item.PurchasedNewSwap && !string.IsNullOrEmpty(appliedSwap.SwappableItem?.SpawnWithId))
4057  {
4058  var container = item.GetComponent<ItemContainer>();
4059  if (container != null)
4060  {
4061  container.SpawnWithId = appliedSwap.SwappableItem.SpawnWithId;
4062  }
4063  /*string[] splitIdentifier = appliedSwap.SwappableItem.SpawnWithId.Split(',');
4064  foreach (string id in splitIdentifier)
4065  {
4066  ItemPrefab itemToSpawn = ItemPrefab.Find(name: null, identifier: id.Trim());
4067  if (itemToSpawn == null)
4068  {
4069  DebugConsole.ThrowError($"Failed to spawn an item inside the purchased {item.Name} (could not find an item with the identifier \"{id}\").");
4070  }
4071  else
4072  {
4073  var spawnedItem = new Item(itemToSpawn, Vector2.Zero, null);
4074  item.OwnInventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots, createNetworkEvent: false);
4075  Spawner?.AddToSpawnQueue(itemToSpawn, item.OwnInventory, spawnIfInventoryFull: false);
4076  }
4077  }*/
4078  }
4079  item.PurchasedNewSwap = false;
4080  }
4081 
4082  Version savedVersion = submarine?.Info.GameVersion;
4083  if (element.Document?.Root != null && element.Document.Root.Name.ToString().Equals("gamesession", StringComparison.OrdinalIgnoreCase))
4084  {
4085  //character inventories are loaded from the game session file - use the version number of the saved game session instead of the sub
4086  //(the sub may have already been saved and up-to-date, even though the character inventories aren't)
4087  savedVersion = new Version(element.Document.Root.GetAttributeString("version", "0.0.0.0"));
4088  }
4089 
4090  float prevCondition = item.condition;
4091  if (savedVersion != null)
4092  {
4093  SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, savedVersion);
4094  }
4095 
4096  if (element.GetAttribute("conditionpercentage") != null)
4097  {
4098  item.condition = element.GetAttributeFloat("conditionpercentage", 100.0f) / 100.0f * item.MaxCondition;
4099  }
4100  else
4101  {
4102  //backwards compatibility
4103  item.condition = element.GetAttributeFloat("condition", item.condition);
4104  //if the item was in full condition considering the unmodified health
4105  //(not taking possible HealthMultipliers added by mods into account),
4106  //make sure it stays in full condition
4107  if (item.condition > 0)
4108  {
4109  bool wasFullCondition = prevCondition >= item.Prefab.Health;
4110  if (wasFullCondition)
4111  {
4112  item.condition = item.MaxCondition;
4113  }
4114  item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition);
4115  }
4116  }
4117  item.lastSentCondition = item.prevCondition = item.condition;
4118  item.RecalculateConditionValues();
4119  item.SetActiveSprite();
4120 
4121  foreach (ItemComponent component in item.components)
4122  {
4123  if (component.Parent != null && component.InheritParentIsActive) { component.IsActive = component.Parent.IsActive; }
4124  component.OnItemLoaded();
4125  }
4126 
4127  item.FullyInitialized = true;
4128 
4129  return item;
4130  }
4131 
4132  public override XElement Save(XElement parentElement)
4133  {
4134  XElement element = new XElement("Item");
4135 
4136  element.Add(
4137  new XAttribute("name", Prefab.OriginalName),
4138  new XAttribute("identifier", Prefab.Identifier),
4139  new XAttribute("ID", ID),
4140  new XAttribute("markedfordeconstruction", deconstructItems.Contains(this)));
4141 
4142  if (PendingItemSwap != null)
4143  {
4144  element.Add(new XAttribute("pendingswap", PendingItemSwap.Identifier));
4145  }
4146 
4147  if (Rotation != 0f) { element.Add(new XAttribute("rotation", Rotation)); }
4148 
4149  if (ItemPrefab.Prefabs.IsOverride(Prefab)) { element.Add(new XAttribute("isoverride", "true")); }
4150  if (FlippedX) { element.Add(new XAttribute("flippedx", true)); }
4151  if (FlippedY) { element.Add(new XAttribute("flippedy", true)); }
4152 
4153  if (AvailableSwaps.Any())
4154  {
4155  element.Add(new XAttribute("availableswaps", string.Join(',', AvailableSwaps.Select(s => s.Identifier))));
4156  }
4157 
4158  if (!MathUtils.NearlyEqual(healthMultiplier, 1.0f))
4159  {
4160  element.Add(new XAttribute("healthmultiplier", HealthMultiplier.ToString("G", CultureInfo.InvariantCulture)));
4161  }
4162 
4163  Item rootContainer = RootContainer ?? this;
4164  System.Diagnostics.Debug.Assert(Submarine != null || rootContainer.ParentInventory?.Owner is Character);
4165 
4166  Vector2 subPosition = Submarine == null ? Vector2.Zero : Submarine.HiddenSubPosition;
4167 
4168  int width = ResizeHorizontal ? rect.Width : defaultRect.Width;
4169  int height = ResizeVertical ? rect.Height : defaultRect.Height;
4170  element.Add(new XAttribute("rect",
4171  (int)(rect.X - subPosition.X) + "," +
4172  (int)(rect.Y - subPosition.Y) + "," +
4173  width + "," + height));
4174 
4175  if (linkedTo != null && linkedTo.Count > 0)
4176  {
4177  bool isOutpost = Submarine != null && Submarine.Info.IsOutpost;
4178  var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved && (l.Removed == Removed) && (l.Submarine == null || l.Submarine.Info.IsOutpost == isOutpost));
4179  element.Add(new XAttribute("linked", string.Join(",", saveableLinked.Select(l => l.ID.ToString()))));
4180  }
4181 
4183 
4184  foreach (ItemComponent ic in components)
4185  {
4186  ic.Save(element);
4187  }
4188 
4189  foreach (var upgrade in Upgrades)
4190  {
4191  upgrade.Save(element);
4192  }
4193 
4194  statManager?.Save(element);
4195 
4196  element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture)));
4197 
4198  var conditionAttribute = element.GetAttribute("condition");
4199  if (conditionAttribute != null) { conditionAttribute.Remove(); }
4200 
4201  parentElement.Add(element);
4202 
4203  return element;
4204  }
4205 
4206  public virtual void Reset()
4207  {
4208  var holdable = GetComponent<Holdable>();
4209  bool wasAttached = holdable?.Attached ?? false;
4210 
4212  Sprite.ReloadXML();
4214  condition = MaxCondition;
4215  components.ForEach(c => c.Reset());
4216  if (wasAttached)
4217  {
4218  holdable.AttachToWall();
4219  }
4220  }
4221 
4222  public override void OnMapLoaded()
4223  {
4224  FindHull();
4225 
4226  foreach (ItemComponent ic in components)
4227  {
4228  ic.OnMapLoaded();
4229  }
4230  }
4231 
4236  public override void ShallowRemove()
4237  {
4238  base.ShallowRemove();
4239 
4240  foreach (ItemComponent ic in components)
4241  {
4242  ic.ShallowRemove();
4243  }
4244  RemoveFromLists();
4245 
4246  if (body != null)
4247  {
4248  body.Remove();
4249  body = null;
4250  }
4251 
4252  GameMain.LuaCs.Hook.Call("item.removed", this);
4253  }
4254 
4255  public override void Remove()
4256  {
4257  if (Removed)
4258  {
4259  DebugConsole.ThrowError("Attempting to remove an already removed item (" + Name + ")\n" + Environment.StackTrace.CleanupStackTrace());
4260  return;
4261  }
4262  DebugConsole.Log("Removing item " + Name + " (ID: " + ID + ")");
4263 
4264  base.Remove();
4265 
4266  foreach (Character character in Character.CharacterList)
4267  {
4268  if (character.SelectedItem == this) { character.SelectedItem = null; }
4269  if (character.SelectedSecondaryItem == this) { character.SelectedSecondaryItem = null; }
4270  }
4271 
4272  Door door = GetComponent<Door>();
4273  Ladder ladder = GetComponent<Ladder>();
4274  if (door != null || ladder != null)
4275  {
4276  foreach (WayPoint wp in WayPoint.WayPointList)
4277  {
4278  if (door != null && wp.ConnectedDoor == door) { wp.ConnectedGap = null; }
4279  if (ladder != null && wp.Ladders == ladder) { wp.Ladders = null; }
4280  }
4281  }
4282 
4283  connections?.Clear();
4284 
4285  if (parentInventory != null)
4286  {
4287  if (parentInventory is CharacterInventory characterInventory)
4288  {
4289  characterInventory.RemoveItem(this, tryEquipFromSameStack: true);
4290  }
4291  else
4292  {
4293  parentInventory.RemoveItem(this);
4294  }
4295  parentInventory = null;
4296  }
4297 
4298  foreach (ItemComponent ic in components)
4299  {
4300  ic.Remove();
4301 #if CLIENT
4302  ic.GuiFrame = null;
4303 #endif
4304  }
4305 
4306  RemoveFromLists();
4307 
4308  if (body != null)
4309  {
4310  body.Remove();
4311  body = null;
4312  }
4313 
4314  CurrentHull = null;
4315 
4316  if (StaticFixtures != null)
4317  {
4318  foreach (Fixture fixture in StaticFixtures)
4319  {
4320  //if the world is null, the body has already been removed
4321  //happens if the sub the fixture is attached to is removed before the item
4322  if (fixture.Body?.World == null) { continue; }
4323  fixture.Body.Remove(fixture);
4324  }
4325  StaticFixtures.Clear();
4326  }
4327 
4328  foreach (Item it in ItemList)
4329  {
4330  if (it.linkedTo.Contains(this))
4331  {
4332  it.linkedTo.Remove(this);
4333  }
4334  }
4335 
4336  RemoveProjSpecific();
4337 
4338  GameMain.LuaCs.Hook.Call("item.removed", this);
4339  }
4340 
4341  private void RemoveFromLists()
4342  {
4343  ItemList.Remove(this);
4344  dangerousItems.Remove(this);
4345  repairableItems.Remove(this);
4346  sonarVisibleItems.Remove(this);
4347  cleanableItems.Remove(this);
4348  deconstructItems.Remove(this);
4349  RemoveFromDroppedStack(allowClientExecute: true);
4350  }
4351 
4352  partial void RemoveProjSpecific();
4353 
4354  public static void RemoveByPrefab(ItemPrefab prefab)
4355  {
4356  if (ItemList == null) { return; }
4357  List<Item> list = new List<Item>(ItemList);
4358  foreach (Item item in list)
4359  {
4360  if (((MapEntity)item).Prefab == prefab)
4361  {
4362  item.Remove();
4363  }
4364  }
4365  }
4366  }
4368  {
4369  public Character Character { get; set; }
4370  public Character User { get; set; }
4371  public Item Item { get; set; }
4372  public Limb TargetLimb { get; set; }
4373 
4374  public AbilityApplyTreatment(Character user, Character target, Item item, Limb limb)
4375  {
4376  Character = target;
4377  User = user;
4378  Item = item;
4379  TargetLimb = limb;
4380  }
4381  }
4382 }
bool NeedsUpdate
Does the AI target do something that requires Update() to be called (e.g. static targets don't need t...
AbilityApplyTreatment(Character user, Character target, Item item, Limb limb)
Attacks are used to deal damage to characters, structures and items. They can be defined in the weapo...
float GetItemDamage(float deltaTime, float multiplier=1)
static bool BlocksInteraction(InteractionType interactionType)
bool CanInteractWith(Character c, float maxDist=200.0f, bool checkVisibility=true, bool skipDistanceCheck=false)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
Item SelectedSecondaryItem
The secondary selected item. It's an item other than a device (see SelectedItem), e....
readonly ContentPath Path
Definition: ContentFile.cs:137
float GetAttributeFloat(string key, float def)
Rectangle GetAttributeRect(string key, in Rectangle def)
XAttribute? GetAttribute(string name)
Identifier GetAttributeIdentifier(string key, string def)
static CoroutineStatus Running
AITarget AiTarget
Definition: Entity.cs:55
static EntitySpawner Spawner
Definition: Entity.cs:31
Submarine Submarine
Definition: Entity.cs:53
const ushort NullEntityID
Definition: Entity.cs:14
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
AITarget aiTarget
Definition: Entity.cs:33
static GameSession?? GameSession
Definition: GameMain.cs:88
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static GameClient Client
Definition: GameMain.cs:188
static LuaCsSetup LuaCs
Definition: GameMain.cs:26
static Hull FindHull(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any)
void ApplyFlowForces(float deltaTime, Item item)
ushort GetOffsetId(XElement element)
Definition: IdRemap.cs:86
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...
readonly ImmutableArray< ItemInventory > OwnInventories
Identifier??????? ContainerIdentifier
Can be used by status effects or conditionals to check what item this item is contained inside
bool IsPlayerTeamInteractable
Checks both NonInteractable and NonPlayerTeamInteractable
IReadOnlyCollection< Identifier > GetTags()
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character=null, Limb limb=null, Entity useTarget=null, bool isNetworkEvent=false, bool checkCondition=true, Vector2? worldPosition=null)
void RecalculateConditionValues()
Recalculates the item's maximum condition, condition percentage and whether it's in full condition....
bool NonPlayerTeamInteractable
Use IsPlayerInteractable to also check NonInteractable
static IReadOnlyCollection< Item > CleanableItems
Items that may potentially need to be cleaned up (pickable, not attached to a wall,...
void ApplyTreatment(Character user, Character character, Limb targetLimb)
bool IsShootable
Should the item's Use method be called with the "Use" or with the "Shoot" key?
static void RemoveByPrefab(ItemPrefab prefab)
override Quad2D GetTransformedQuad()
void DisableDrawableComponent(IDrawableComponent drawable)
readonly HashSet< ItemPrefab > AvailableSwaps
bool IsContained
Can be used by status effects or conditionals to check whether the item is contained inside something
void ReplaceTag(string tag, string newTag)
List<(object obj, SerializableProperty property)> GetProperties< T >()
bool?? Indestructible
Per-instance value - if not set, the value of the prefab is used.
bool ConditionIncreasedRecently
Return true if the condition of this item increased within the last second.
float PositionY
Can be used to move the item from XML (e.g. to correct the positions of items whose sprite origin has...
bool RequireAimToSecondaryUse
If true, the user has to hold the "aim" key before secondary use is registered. True by default.
string? DescriptionTag
Can be used to set a localized description via StatusEffects
void EnableDrawableComponent(IDrawableComponent drawable)
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...
bool TryFindController(out Controller controller, ImmutableArray< Identifier >? tags=null)
new float? SightRange
Can be used by status effects or conditionals to modify the sight range
bool ConditionalMatches(PropertyConditional conditional)
IEnumerable< Item > GetStackedItems()
Returns this item and all the other items in the stack (either in the same inventory slot,...
IEnumerable< InvSlotType > AllowedSlots
void ReplaceTag(Identifier tag, Identifier newTag)
void ResetWaterDragCoefficient()
Removes the override value -> falls back to using the original value defined in the xml.
override void Move(Vector2 amount, bool ignoreContacts=true)
IReadOnlyList< ISerializableEntity > AllPropertyObjects
void SendSignal(Signal signal, Connection connection)
void SetTransform(Vector2 simPosition, float rotation, bool findNewHull=true, bool setPrevTransform=true)
void Use(float deltaTime, Character user=null, Limb targetLimb=null, Entity useTarget=null, Character userForOnUsedEvent=null)
static void UpdateHulls()
goes through every item and re-checks which hull they are in
override void FlipX(bool relativeToSub)
Flip the entity horizontally
override void Update(float deltaTime, Camera cam)
Rectangle TransformTrigger(Rectangle trigger, bool world=false)
override void FlipY(bool relativeToSub)
Flip the entity vertically
void AssignCampaignInteractionType(CampaignMode.InteractionType interactionType, IEnumerable< Client > targetClients=null)
bool IsInteractable(Character character)
Returns interactibility based on whether the character is on a player team
bool IgnoreByAI(Character character)
bool IsOwnedBy(Entity entity)
Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool callOnItemLoaded=true, ushort id=Entity.NullEntityID)
Creates a new item
bool TryInteract(Character user, bool ignoreRequiredItems=false, bool forceSelectKey=false, bool forceUseKey=false)
bool Illegitimate
Item shouldn't be in the player's inventory. If the guards find it, they will consider it as a theft.
void SecondaryUse(float deltaTime, Character character=null)
static Item Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound=true)
void AddComponent(ItemComponent component)
float ConditionPercentageRelativeToDefaultMaxCondition
Condition percentage disregarding MaxRepairConditionMultiplier (i.e. this can go above 100% if the it...
static readonly ImmutableArray<(Identifier Input, Identifier Output)> connectionPairs
bool HasIdentifierOrTags(IEnumerable< Identifier > identifiersOrTags)
bool HasTag(IEnumerable< Identifier > allowedTags)
List< T > GetConnectedComponents< T >(bool recursive=false, bool allowTraversingBackwards=true, Func< Connection, bool > connectionFilter=null)
Note: This function generates garbage and might be a bit too heavy to be used once per frame.
new float? SoundRange
Can be used by status effects or conditionals to modify the sound range
override XElement Save(XElement parentElement)
Character Equipper
Which character equipped this item? May not be the same character as the one who it's equipped on (yo...
bool IsInsideTrigger(Vector2 worldPosition)
void CheckCleanable()
Recheck if the item needs to be included in the list of cleanable items
float LastEatenTime
Timing.TotalTimeUnpaused when some character was last eating the item
bool IsInsideTrigger(Vector2 worldPosition, out Rectangle transformedTrigger)
static void UpdatePendingConditionUpdates(float deltaTime)
static IReadOnlyCollection< Item > RepairableItems
Items that have one more more Repairable component
bool ConditionalMatches(PropertyConditional conditional, bool checkContainer)
bool FullyInitialized
Has everything in the item been loaded/instantiated/initialized (basically, can be used to check if t...
static Item Load(ContentXElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap)
Instantiate a new item and load its data from the XML element.
bool RequireAimToUse
If true, the user has to hold the "aim" key before use is registered. False by default.
bool IsContainerPreferred(ItemContainer container, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRestriction=false)
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
static IReadOnlyCollection< Item > DangerousItems
void CreateDroppedStack(IEnumerable< Item > items, bool allowClientExecute)
"Merges" the set of items so they behave as one physical object and can be picked up by clicking once...
static readonly List< Item > ItemList
bool PhysicsBodyActive
Can be used by status effects or conditionals to check if the physics body of the item is active
bool DontCleanUp
Can be set by status effects to prevent bots from cleaning up the item
Inventory FindParentInventory(Func< Inventory, bool > predicate)
float PositionX
Can be used to move the item from XML (e.g. to correct the positions of items whose sprite origin has...
Dictionary< Identifier, SerializableProperty > SerializableProperties
void SendSignal(Signal signal, string connectionName)
Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id=Entity.NullEntityID, bool callOnItemLoaded=true)
List< Connection > LastSentSignalRecipients
A list of connections the last signal sent by this item went through
bool AllowDroppingOnSwapWith(Item otherItem)
Is dropping the item allowed when trying to swap it with the other item
bool StolenDuringRound
Was the item stolen during the current round. Note that it's possible for the items to be found in th...
void SendSignal(string signal, string connectionName)
override void ShallowRemove()
Remove the item so that it doesn't appear to exist in the game world (stop sounds,...
static HashSet< Item > DeconstructItems
Items that have been marked for deconstruction
float? Speed
Can be used by status effects or conditionals to the speed of the item
static IReadOnlyCollection< Item > SonarVisibleItems
Items whose ItemPrefab.SonarSize is larger than 0
float GetQualityModifier(Quality.StatType statType)
bool Combine(Item item, Character user)
BodyType??? BodyType
Can be used by StatusEffects to set the type of the body (if the item has one)
CampaignMode.InteractionType CampaignInteractionType
int GetComponentIndex(ItemComponent component)
string????? SonarLabel
Can be used to modify the AITarget's label using status effects
bool HasAccess(Character character)
Used by the AI to check whether they can (in principle) and are allowed (in practice) to interact wit...
Controller FindController(ImmutableArray< Identifier >? tags=null)
static readonly PrefabCollection< ItemPrefab > Prefabs
static ItemPrefab Find(string name, Identifier identifier)
List< CircuitBoxConnection > CircuitBoxConnections
Circuit box input and output connections that are linked to this connection.
Connection(ContentXElement element, ConnectionPanel connectionPanel, IdRemap idRemap)
The base class for components holding the different functionalities of the item
bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg=null)
void ApplyStatusEffects(ActionType type, float deltaTime, Character character=null, Limb targetLimb=null, Entity useTarget=null, Character user=null, Vector2? worldPosition=null, float afflictionMultiplier=1.0f)
virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg=null)
readonly record struct ItemUseInfo(Item Item, Character User)
virtual void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
void ShallowRemove()
Remove the component so that it doesn't appear to exist in the game world (stop sounds,...
virtual void Drop(Character dropper, bool setTransform=true)
a Character has dropped the item
virtual void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
readonly Dictionary< ActionType, List< StatusEffect > > statusEffectLists
virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
float DegreeOfSuccess(Character character)
Returns 0.0f-1.0f based on how well the Character can use the itemcomponent
virtual bool Pick(Character picker)
a Character has picked the item
readonly LevelType Type
Definition: LevelData.cs:19
readonly string Seed
Definition: LevelData.cs:21
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
object Call(string name, params object[] args)
bool IsHidden
Is the entity hidden due to HiddenInGame being enabled or the layer the entity is in being hidden?
bool IsLayerHidden
Is the layer this entity is in currently hidden? If it is, the entity is not updated and should do no...
readonly List< Upgrade > Upgrades
List of upgrades this item has
static MapEntityPrefab Find(string name, string identifier=null, bool showErrorMessages=true)
Find a matching map entity prefab
override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData=null)
Definition: GameClient.cs:2606
void ApplyForce(Vector2 force, float maxVelocity=NetConfig.MaxPhysicsBodyVelocity)
bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransform=true)
Vector2 GetLocalFront(float? spritesheetRotation=null)
Returns the farthest point towards the forward of the body. For capsules and circles,...
bool SetTransformIgnoreContacts(Vector2 simPosition, float rotation, bool setPrevTransform=true)
readonly ContentFile ContentFile
Definition: Prefab.cs:35
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
Conditionals are used by some in-game mechanics to require one or more conditions to be met for those...
bool Matches(ISerializableEntity? target)
readonly bool TargetContainer
If set to true, the conditionals defined by this element check against the entity containing the targ...
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 Dictionary< Identifier, SerializableProperty > DeserializeProperties(object obj, XElement element=null)
SerializableProperty(PropertyDescriptor property)
static void UpgradeGameVersion(ISerializableEntity entity, ContentXElement configElement, Version savedVersion)
Upgrade the properties of an entity saved with an older version of the game. Properties that should b...
static Dictionary< Identifier, SerializableProperty > GetProperties(object obj)
static void SerializeProperties(ISerializableEntity obj, XElement element, bool saveIfDefault=false, bool ignoreEditable=false)
void ReloadXML()
Works only if there is a name attribute defined for the sprite. For items and structures,...
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
readonly bool AllowWhenBroken
Can the StatusEffect be applied when the item applying it is broken?
void AddNearbyTargets(Vector2 worldPosition, List< ISerializableEntity > targets)
int TargetSlot
Index of the slot the target must be in. Only valid when targeting a Contained item.
readonly LimbType[] targetLimbs
Which types of limbs this effect can target? Only valid when targeting characters or limbs.
readonly ImmutableHashSet< Identifier > TargetIdentifiers
Identifier(s), tag(s) or species name(s) of the entity the effect can target. Null if there's no iden...
virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition=null)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive=false)
bool IsEditable(ISerializableEntity entity)
Interface for entities that the clients can send events to the server
Interface for entities that handle ServerNetObject.ENTITY_POSITION
void WriteIdentifier(Identifier val)
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
AbilityEffectType
Definition: Enums.cs:125