Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs
1 using Microsoft.Xna.Framework;
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
5 using System.Reflection;
6 using System.Xml.Linq;
8 using Barotrauma;
9 using MoonSharp.Interpreter;
10 using Barotrauma.IO;
12 #if CLIENT
13 using Microsoft.Xna.Framework.Graphics;
14 using Barotrauma.Sounds;
15 #endif
16 
18 {
20  {
21 #if CLIENT
25  Vector2 DrawSize { get; }
26 
27  void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null);
28 #endif
29  }
30 
35  {
36  protected Item item;
37 
38  protected string name;
39 
40  private bool isActive;
41 
42  protected bool characterUsable;
43 
44  protected bool canBePicked;
45  protected bool canBeSelected;
46  protected bool canBeCombined;
47  protected bool removeOnCombined;
48 
49  public bool WasUsed, WasSecondaryUsed;
50 
51  public readonly Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
52 
53  public Dictionary<RelatedItem.RelationType, List<RelatedItem>> RequiredItems;
54  public readonly List<RelatedItem> DisabledRequiredItems = new List<RelatedItem>();
55 
56  public readonly List<Skill> RequiredSkills = new List<Skill>();
57 
58  private ItemComponent parent;
60  {
61  get { return parent; }
62  set
63  {
64  if (parent == value) { return; }
66  {
67  if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; }
68  if (value != null) { value.OnActiveStateChanged += SetActiveState; }
69  }
70  parent = value;
71  }
72  }
73 
74 
75  [Serialize(true, IsPropertySaveable.No, description: "If this is a child component of another component, should this component inherit the IsActive state of the parent?")]
76  public bool InheritParentIsActive { get; set; }
77 
79 
80  protected const float CorrectionDelay = 1.0f;
82 
87  public virtual bool DontTransferInventoryBetweenSubs => false;
88 
93  public virtual bool DisallowSellingItemsFromContainer => false;
94 
95  [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes to pick up the item (in seconds).")]
96  public float PickingTime
97  {
98  get;
99  set;
100  }
101 
102  [Serialize("", IsPropertySaveable.No, description: "What to display on the progress bar when this item is being picked.")]
103  public string PickingMsg
104  {
105  get;
106  set;
107  }
108 
109  public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; protected set; }
110 
111  public Action<bool> OnActiveStateChanged;
112 
113  public virtual bool IsActive
114  {
115  get { return isActive; }
116  set
117  {
118 #if CLIENT
119  if (!value)
120  {
121  IsActiveTimer = 0.0f;
122  if (isActive)
123  {
124  StopSounds(ActionType.OnActive);
125  }
126  }
127 #endif
128  if (value != IsActive) { OnActiveStateChanged?.Invoke(value); }
129  isActive = value;
130  }
131  }
132 
133  private bool drawable = true;
134 
137  {
138  get;
139  set;
140  }
141 
142  public List<PropertyConditional> IsActiveConditionals;
143 
144  public bool Drawable
145  {
146  get { return drawable; }
147  set
148  {
149  if (value == drawable) { return; }
150  if (this is not IDrawableComponent)
151  {
152  DebugConsole.ThrowError("Couldn't make \"" + this + "\" drawable (the component doesn't implement the IDrawableComponent interface)");
153  return;
154  }
155 
156  drawable = value;
157  if (drawable)
158  {
160  }
161  else
162  {
164  }
165  }
166  }
167 
168  [Editable, Serialize(false, IsPropertySaveable.No, description: "Can the item be picked up (or interacted with, if the pick action does something else than picking up the item).")] //Editable for doors to do their magic
169  public bool CanBePicked
170  {
171  get { return canBePicked; }
172  set { canBePicked = value; }
173  }
174 
175  [Serialize(false, IsPropertySaveable.No, description: "Should the interface of the item (if it has one) be drawn when the item is equipped.")]
177  {
178  get;
179  protected set;
180  }
181 
182  [Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)]
183  public bool LockGuiFramePosition { get; set; }
184 
185  [Serialize("0,0", IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)]
186  public Point GuiFrameOffset { get; set; }
187 
188  [Serialize(false, IsPropertySaveable.No, description: "Can the item be selected by interacting with it.")]
189  public bool CanBeSelected
190  {
191  get { return canBeSelected; }
192  set { canBeSelected = value; }
193  }
194 
195  [Serialize(false, IsPropertySaveable.No, description: "Can the item be combined with other items of the same type.")]
196  public bool CanBeCombined
197  {
198  get { return canBeCombined; }
199  set { canBeCombined = value; }
200  }
201 
202  [Serialize(false, IsPropertySaveable.No, description: "Should the item be removed if combining it with an other item causes the condition of this item to drop to 0.")]
203  public bool RemoveOnCombined
204  {
205  get { return removeOnCombined; }
206  set { removeOnCombined = value; }
207  }
208 
209  [Serialize(false, IsPropertySaveable.No, description: "Can the \"Use\" action of the item be triggered by characters or just other items/StatusEffects.")]
210  public bool CharacterUsable
211  {
212  get { return characterUsable; }
213  set { characterUsable = value; }
214  }
215 
216  //Remove item if combination results in 0 condition
217  [Serialize(true, IsPropertySaveable.No, description: "Can the properties of the component be edited in-game (only applicable if the component has in-game editable properties)."), Editable()]
218  public bool AllowInGameEditing
219  {
220  get;
221  set;
222  }
223 
225  {
226  get;
227  protected set;
228  }
229 
231  {
232  get;
233  protected set;
234  }
235 
236  [Serialize(false, IsPropertySaveable.No, description: "Should the item be deleted when it's used.")]
237  public bool DeleteOnUse
238  {
239  get;
240  set;
241  }
242 
243  public Item Item
244  {
245  get { return item; }
246  }
247 
248  public string Name
249  {
250  get { return name; }
251  }
252 
253  [Editable, Serialize("", IsPropertySaveable.Yes, translationTextTag: "ItemMsg", description: "A text displayed next to the item when it's highlighted (generally instructs how to interact with the item, e.g. \"[Mouse1] Pick up\").")]
254  public string Msg
255  {
256  get;
257  set;
258  }
259 
261  {
262  get;
263  set;
264  }
265 
266  [Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not forced). Note that there's also a generic BotPriority for all item prefabs.")]
267  public float CombatPriority { get; private set; }
268 
272  [Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
273  public int ManuallySelectedSound { get; private set; }
274 
278  public float Speed => item.Speed;
279 
280  public readonly record struct ItemUseInfo(Item Item, Character User);
281  public readonly NamedEvent<ItemUseInfo> OnUsed = new();
282 
283  public readonly bool InheritStatusEffects;
284 
286  {
287  this.item = item;
288  originalElement = element;
289  name = element.Name.ToString();
291  RequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>();
292 #if CLIENT
293  hasSoundsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length];
294  sounds = new Dictionary<ActionType, List<ItemSound>>();
295 #endif
296 
297  SelectKey = InputType.Select;
298 
299  try
300  {
301  string selectKeyStr = element.GetAttributeString("selectkey", "Select");
302  selectKeyStr = ToolBox.ConvertInputType(selectKeyStr);
303  SelectKey = (InputType)Enum.Parse(typeof(InputType), selectKeyStr, true);
304  }
305  catch (Exception e)
306  {
307  DebugConsole.ThrowError("Invalid select key in " + element + "!", e,
308  contentPackage: element.ContentPackage);
309  }
310 
311  PickKey = InputType.Select;
312 
313  try
314  {
315  string pickKeyStr = element.GetAttributeString("pickkey", "Select");
316  pickKeyStr = ToolBox.ConvertInputType(pickKeyStr);
317  PickKey = (InputType)Enum.Parse(typeof(InputType), pickKeyStr, true);
318  }
319  catch (Exception e)
320  {
321  DebugConsole.ThrowError("Invalid pick key in " + element + "!", e,
322  contentPackage: element.ContentPackage);
323  }
324 
326  ParseMsg();
327 
328  string inheritRequiredSkillsFrom = element.GetAttributeString("inheritrequiredskillsfrom", "");
329  if (!string.IsNullOrEmpty(inheritRequiredSkillsFrom))
330  {
331  var component = item.Components.Find(ic => ic.Name.Equals(inheritRequiredSkillsFrom, StringComparison.OrdinalIgnoreCase));
332  if (component == null)
333  {
334  DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its required skills from \"{inheritRequiredSkillsFrom}\", but a component of that type couldn't be found.",
335  contentPackage: element.ContentPackage);
336  }
337  else
338  {
339  RequiredSkills = component.RequiredSkills;
340  }
341  }
342 
343  string inheritStatusEffectsFrom = element.GetAttributeString("inheritstatuseffectsfrom", "");
344  if (!string.IsNullOrEmpty(inheritStatusEffectsFrom))
345  {
346  InheritStatusEffects = true;
347  var component = item.Components.Find(ic => ic.Name.Equals(inheritStatusEffectsFrom, StringComparison.OrdinalIgnoreCase));
348  if (component == null)
349  {
350  DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its StatusEffects from \"{inheritStatusEffectsFrom}\", but a component of that type couldn't be found.",
351  contentPackage: element.ContentPackage);
352  }
353  else if (component.statusEffectLists != null)
354  {
355  statusEffectLists ??= new Dictionary<ActionType, List<StatusEffect>>();
356  foreach (KeyValuePair<ActionType, List<StatusEffect>> kvp in component.statusEffectLists)
357  {
358  if (!statusEffectLists.TryGetValue(kvp.Key, out List<StatusEffect> effectList))
359  {
360  effectList = new List<StatusEffect>();
361  statusEffectLists.Add(kvp.Key, effectList);
362  }
363  effectList.AddRange(kvp.Value);
364  }
365  }
366  }
367 
368  foreach (var subElement in element.Elements())
369  {
370  switch (subElement.Name.ToString().ToLowerInvariant())
371  {
372  case "activeconditional":
373  case "isactiveconditional":
374  case "isactive":
375  IsActiveConditionals ??= new List<PropertyConditional>();
377  break;
378  case "requireditem":
379  case "requireditems":
380  SetRequiredItems(subElement);
381  break;
382  case "requiredskill":
383  case "requiredskills":
384  if (subElement.GetAttribute("name") != null)
385  {
386  DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill.",
387  contentPackage: element.ContentPackage);
388  continue;
389  }
390 
391  Identifier skillIdentifier = subElement.GetAttributeIdentifier("identifier", "");
392  RequiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0)));
393  break;
394  case "statuseffect":
395  statusEffectLists ??= new Dictionary<ActionType, List<StatusEffect>>();
396  LoadStatusEffect(subElement);
397  break;
398  default:
399  if (LoadElemProjSpecific(subElement)) { break; }
400  ItemComponent ic = Load(subElement, item, false);
401  if (ic == null) { break; }
402 
403  ic.Parent = this;
404  if (ic.InheritParentIsActive)
405  {
406  ic.IsActive = isActive;
407  OnActiveStateChanged += ic.SetActiveState;
408  }
409 
410  item.AddComponent(ic);
411  break;
412  }
413  }
414 
415  void LoadStatusEffect(ContentXElement subElement)
416  {
417  var statusEffect = StatusEffect.Load(subElement, item.Name + ", " + GetType().Name);
418  if (!statusEffectLists.TryGetValue(statusEffect.type, out List<StatusEffect> effectList))
419  {
420  effectList = new List<StatusEffect>();
421  statusEffectLists.Add(statusEffect.type, effectList);
422  }
423  effectList.Add(statusEffect);
424  }
425  }
426 
427  private void SetActiveState(bool isActive)
428  {
429  IsActive = isActive;
430  }
431 
432  public void SetRequiredItems(ContentXElement element, bool allowEmpty = false)
433  {
434  bool returnEmpty = false;
435 #if CLIENT
436  returnEmpty = Screen.Selected == GameMain.SubEditorScreen;
437 #endif
438  RelatedItem ri = RelatedItem.Load(element, returnEmpty, item.Name);
439  if (ri != null)
440  {
441  if (ri.Identifiers.Count == 0)
442  {
443  DisabledRequiredItems.Add(ri);
444  }
445  else
446  {
447  if (!RequiredItems.ContainsKey(ri.Type))
448  {
449  RequiredItems.Add(ri.Type, new List<RelatedItem>());
450  }
451  RequiredItems[ri.Type].Add(ri);
452  }
453  }
454  else if (!allowEmpty)
455  {
456  DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers.",
457  contentPackage: element.ContentPackage);
458  }
459  }
460 
461  public virtual void Move(Vector2 amount, bool ignoreContacts = false) { }
462 
464  public virtual bool Pick(Character picker)
465  {
466  return false;
467  }
468 
469  public virtual bool Select(Character character)
470  {
471  return CanBeSelected;
472  }
473 
475  public virtual void Drop(Character dropper, bool setTransform = true) { }
476 
478  public virtual bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
479  {
480  return false;
481  }
482 
483  public virtual bool UpdateWhenInactive => false;
484 
485  //called when isActive is true and condition > 0.0f
486  public virtual void Update(float deltaTime, Camera cam)
487  {
488  ApplyStatusEffects(ActionType.OnActive, deltaTime);
489  }
490 
491  //called when isActive is true and condition == 0.0f
492  public virtual void UpdateBroken(float deltaTime, Camera cam)
493  {
494 #if CLIENT
495  StopSounds(ActionType.OnActive);
496 #endif
497  }
498 
499  //called when the item is equipped and the "use" key is pressed
500  //returns true if the item was used succesfully (not out of ammo, reloading, etc)
501  public virtual bool Use(float deltaTime, Character character = null)
502  {
503  return characterUsable || character == null;
504  }
505 
506  //called when the item is equipped and the "aim" key is pressed or when the item is selected if it doesn't require aiming.
507  public virtual bool SecondaryUse(float deltaTime, Character character = null)
508  {
509  return false;
510  }
511 
512  //called when the item is placed in a "limbslot"
513  public virtual void Equip(Character character) { }
514 
515  //called then the item is dropped or dragged out of a "limbslot"
516  public virtual void Unequip(Character character) { }
517 
518  public virtual void ReceiveSignal(Signal signal, Connection connection)
519  {
520  switch (connection.Name)
521  {
522  case "activate":
523  case "use":
524  case "trigger_in":
525  if (signal.value != "0")
526  {
527  item.Use(1.0f, user: signal.sender);
528  }
529  break;
530  case "toggle":
531  if (signal.value != "0")
532  {
533  IsActive = !isActive;
534  }
535  break;
536  case "set_active":
537  case "set_state":
538  IsActive = signal.value != "0";
539  break;
540  }
541  }
542 
543  public virtual bool Combine(Item item, Character user)
544  {
545  if (canBeCombined && this.item.Prefab == item.Prefab &&
546  item.Condition > 0.0f && this.item.Condition > 0.0f &&
547  !item.IsFullCondition && !this.item.IsFullCondition)
548  {
549  float transferAmount = Math.Min(item.Condition, this.item.MaxCondition - this.item.Condition);
550 
551  if (MathUtils.NearlyEqual(transferAmount, 0.0f)) { return false; }
552  if (removeOnCombined)
553  {
554  if (item.Condition - transferAmount <= 0.0f)
555  {
556  if (item.ParentInventory != null)
557  {
558  if (item.ParentInventory.Owner is Character owner && owner.HeldItems.Contains(item))
559  {
560  item.Unequip(owner);
561  }
563  }
564  RemoveItem(item);
565  }
566  else
567  {
568  item.Condition -= transferAmount;
569  }
570  if (this.Item.Condition + transferAmount <= 0.0f)
571  {
572  if (this.Item.ParentInventory != null)
573  {
574  if (this.Item.ParentInventory.Owner is Character owner && owner.HeldItems.Contains(this.Item))
575  {
576  this.Item.Unequip(owner);
577  }
578  this.Item.ParentInventory.RemoveItem(this.Item);
579  }
580  RemoveItem(this.Item);
581  }
582  else
583  {
584  this.Item.Condition += transferAmount;
585  }
586  static void RemoveItem(Item item)
587  {
588  if (Screen.Selected is { IsEditor: true })
589  {
590  item?.Remove();
591  }
592  else
593  {
595  }
596  }
597  }
598  else
599  {
600  this.Item.Condition += transferAmount;
601  item.Condition -= transferAmount;
602  }
603  return true;
604  }
605  return false;
606  }
607 
608  public void Remove()
609  {
610 #if CLIENT
611  if (loopingSoundChannel != null)
612  {
613  loopingSoundChannel.Dispose();
614  loopingSoundChannel = null;
615  }
616 
617  //no need to Dispose these - SoundManager will do it when it when it needs a free channel and the sound has stopped playing
618  //disposing immediately on Remove will for example prevent explosives from playing a sound if the explosion removes the item
619  /*foreach (SoundChannel channel in playingOneshotSoundChannels)
620  {
621  channel.Dispose();
622  }*/
623 
624  if (GuiFrame != null)
625  {
626  GUI.RemoveFromUpdateList(GuiFrame, true);
628  GuiFrame = null;
629  }
630 #endif
631 
632  if (delayedCorrectionCoroutine != null)
633  {
634  CoroutineManager.StopCoroutines(delayedCorrectionCoroutine);
636  }
637 
639  }
640 
645  public void ShallowRemove()
646  {
647 #if CLIENT
648  if (loopingSoundChannel != null)
649  {
650  loopingSoundChannel.Dispose();
651  loopingSoundChannel = null;
652  }
653 #endif
654 
656  }
657 
658  protected virtual void ShallowRemoveComponentSpecific()
659  {
661  }
662 
663 
664  protected virtual void RemoveComponentSpecific()
665  {
666  }
667 
668  protected string GetTextureDirectory(ContentXElement subElement)
669  => subElement.DoesAttributeReferenceFileNameAlone("texture") ? Path.GetDirectoryName(item.Prefab.FilePath) : string.Empty;
670 
671  public bool HasRequiredSkills(Character character)
672  {
673  return HasRequiredSkills(character, out Skill temp);
674  }
675 
676  public bool HasRequiredSkills(Character character, out Skill insufficientSkill)
677  {
678  foreach (Skill skill in RequiredSkills)
679  {
680  float characterLevel = character.GetSkillLevel(skill.Identifier);
681  if (characterLevel < skill.Level * GetSkillMultiplier())
682  {
683  insufficientSkill = skill;
684  return false;
685  }
686  }
687  insufficientSkill = null;
688  return true;
689  }
690 
691  public virtual float GetSkillMultiplier() { return 1; }
692 
697  public float DegreeOfSuccess(Character character)
698  {
699  return DegreeOfSuccess(character, RequiredSkills);
700  }
701 
706  public float DegreeOfSuccess(Character character, List<Skill> requiredSkills)
707  {
708  if (requiredSkills.Count == 0) return 1.0f;
709 
710  if (character == null)
711  {
712  string errorMsg = "ItemComponent.DegreeOfSuccess failed (character was null).\n" + Environment.StackTrace.CleanupStackTrace();
713  DebugConsole.ThrowError(errorMsg);
714  GameAnalyticsManager.AddErrorEventOnce("ItemComponent.DegreeOfSuccess:CharacterNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
715  return 0.0f;
716  }
717 
718  float skillSuccessSum = 0.0f;
719  for (int i = 0; i < requiredSkills.Count; i++)
720  {
721  float characterLevel = character.GetSkillLevel(requiredSkills[i].Identifier);
722  skillSuccessSum += (characterLevel - requiredSkills[i].Level);
723  }
724  float average = skillSuccessSum / requiredSkills.Count;
725 
726  return ((average + 100.0f) / 2.0f) / 100.0f;
727  }
728 
729  public virtual void FlipX(bool relativeToSub) { }
730 
731  public virtual void FlipY(bool relativeToSub) { }
732 
736  public bool IsEmpty(Character user) =>
737  !HasRequiredContainedItems(user, addMessage: false) ||
738  (Item.OwnInventory != null && !Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
739 
740  public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null)
741  {
742  if (!RequiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; }
743  if (item.OwnInventory == null) { return false; }
744 
745  foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Contained])
746  {
747  if (!ri.CheckRequirements(user, item))
748  {
749 #if CLIENT
750  msg ??= ri.Msg;
751  if (addMessage && !msg.IsNullOrEmpty())
752  {
753  GUI.AddMessage(msg, Color.Red);
754  }
755 #endif
756  return false;
757  }
758  }
759 
760  return true;
761  }
762 
766  public virtual bool HasAccess(Character character)
767  {
768  if (character.IsBot && item.IgnoreByAI(character)) { return false; }
769  if (!item.IsInteractable(character)) { return false; }
770  if (RequiredItems.Count == 0) { return true; }
771  if (character.Inventory != null && RequiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List<RelatedItem> relatedItems))
772  {
773  foreach (RelatedItem relatedItem in relatedItems)
774  {
775  foreach (Item otherItem in character.Inventory.AllItems)
776  {
777  if (relatedItem.MatchesItem(otherItem))
778  {
779  if (otherItem.GetComponent<IdCard>() is IdCard idCard)
780  {
781  if (!CheckIdCardAccess(relatedItem, idCard))
782  {
783  continue;
784  }
785  }
786  return true;
787  }
788  }
789  }
790  }
791  return false;
792  }
793 
797  private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard)
798  {
799  if (item.Submarine != null && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle)
800  {
801  //id cards don't work in enemy subs (except on items that only require the default "idcard" tag)
802  if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard"))
803  {
804  return false;
805  }
807  {
808  return false;
809  }
810  }
811  return true;
812  }
813 
814  public virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
815  {
816  if (RequiredItems.None()) { return true; }
817  if (character.Inventory == null) { return false; }
818  bool hasRequiredItems = false;
819  bool canContinue = true;
820  if (RequiredItems.ContainsKey(RelatedItem.RelationType.Equipped))
821  {
822  foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Equipped])
823  {
824  canContinue = CheckItems(ri, character.HeldItems);
825  if (!canContinue) { break; }
826  }
827  }
828  if (canContinue)
829  {
830  if (RequiredItems.ContainsKey(RelatedItem.RelationType.Picked))
831  {
832  foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Picked])
833  {
834  if (!CheckItems(ri, character.Inventory.AllItems)) { break; }
835  }
836  }
837  }
838 
839 #if CLIENT
840  if (!hasRequiredItems && addMessage && !msg.IsNullOrEmpty())
841  {
842  GUI.AddMessage(msg, Color.Red);
843  }
844 #endif
845  return hasRequiredItems;
846 
847  bool CheckItems(RelatedItem relatedItem, IEnumerable<Item> itemList)
848  {
849  bool Predicate(Item it)
850  {
851  if (it == null || it.Condition <= 0.0f || !relatedItem.MatchesItem(it)) { return false; }
852  if (it.GetComponent<IdCard>() is IdCard idCard)
853  {
854  if (!CheckIdCardAccess(relatedItem, idCard))
855  {
856  return false;
857  }
858  }
859  return true;
860  };
861  bool shouldBreak = false;
862  bool inEditor = false;
863 #if CLIENT
864  inEditor = Screen.Selected == GameMain.SubEditorScreen;
865 #endif
866  if (relatedItem.IgnoreInEditor && inEditor)
867  {
868  hasRequiredItems = true;
869  }
870  else if (relatedItem.IsOptional)
871  {
872  if (!hasRequiredItems)
873  {
874  hasRequiredItems = itemList.Any(Predicate);
875  }
876  }
877  else
878  {
879  if (itemList.Any(Predicate))
880  {
881  hasRequiredItems = !relatedItem.RequireEmpty;
882  }
883  else
884  {
885  hasRequiredItems = relatedItem.MatchOnEmpty || relatedItem.RequireEmpty;
886  }
887  if (!hasRequiredItems)
888  {
889  shouldBreak = true;
890  }
891  }
892  if (!hasRequiredItems)
893  {
894  if (msg == null && !relatedItem.Msg.IsNullOrEmpty())
895  {
896  msg = relatedItem.Msg;
897  }
898  }
899  return !shouldBreak;
900  }
901  }
902 
903  public 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)
904  {
905  if (statusEffectLists == null) { return; }
906 
907  if (!statusEffectLists.TryGetValue(type, out List<StatusEffect> statusEffects)) { return; }
908 
909  bool broken = item.Condition <= 0.0f;
910  bool reducesCondition = false;
911  foreach (StatusEffect effect in statusEffects)
912  {
913  if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; }
914  if (user != null) { effect.SetUser(user); }
915  effect.AfflictionMultiplier = afflictionMultiplier;
916  var c = character;
917  if (user != null && effect.HasTargetType(StatusEffect.TargetType.Character) && !effect.HasTargetType(StatusEffect.TargetType.UseTarget))
918  {
919  // A bit hacky, but fixes MeleeWeapons targeting the use target instead of the attacker. Also applies to Projectiles and Throwables, or other callers that passes the user.
920  c = user;
921  }
922  item.ApplyStatusEffect(effect, type, deltaTime, c, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
923  effect.AfflictionMultiplier = 1.0f;
924  reducesCondition |= effect.ReducesItemCondition();
925  }
926  //if any of the effects reduce the item's condition, set the user for OnBroken effects as well
927  if (reducesCondition && user != null && type != ActionType.OnBroken)
928  {
929  foreach (ItemComponent ic in item.Components)
930  {
931  if (ic.statusEffectLists == null || !ic.statusEffectLists.TryGetValue(ActionType.OnBroken, out List<StatusEffect> brokenEffects)) { continue; }
932  foreach (var brokenEffect in brokenEffects)
933  {
934  brokenEffect.SetUser(user);
935  }
936  }
937  }
938 
939 #if CLIENT
940  HintManager.OnStatusEffectApplied(this, type, character);
941 #endif
942  }
943 
944  public virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
945  {
946  if (componentElement != null)
947  {
948  foreach (XAttribute attribute in componentElement.Attributes())
949  {
950  if (!SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; }
951  if (property.OverridePrefabValues ||
952  !usePrefabValues ||
953  (isItemSwap && property.GetAttribute<Editable>() is { TransferToSwappedItem: true }))
954  {
955  property.TrySetValue(this, attribute.Value);
956  }
957  }
958  ParseMsg();
959  OverrideRequiredItems(componentElement);
960  }
961 #if CLIENT
962  if (GuiFrame != null)
963  {
965  if (guiFrameDragHandle != null)
966  {
967  guiFrameDragHandle.Enabled = !LockGuiFramePosition;
968  }
969  }
970 #endif
972  }
973 
977  public virtual void OnMapLoaded() { }
978 
982  public virtual void OnItemLoaded() { }
983 
984  public virtual void OnScaleChanged() { }
985 
989  public virtual void OnInventoryChanged() { }
990 
991  public static ItemComponent Load(ContentXElement element, Item item, bool errorMessages = true)
992  {
993  Type type;
994  Identifier typeName = element.NameAsIdentifier();
995  try
996  {
997  // Get the type of a specified class.
998  type = ReflectionUtils.GetDerivedNonAbstract<ItemComponent>().Append(typeof(ItemComponent)).FirstOrDefault(t => t.Name == typeName);
999  if (type == null)
1000  {
1001  if (errorMessages)
1002  {
1003  DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})",
1004  contentPackage: element.ContentPackage);
1005  }
1006  return null;
1007  }
1008  }
1009  catch (Exception e)
1010  {
1011  if (errorMessages)
1012  {
1013  DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e,
1014  contentPackage: element.ContentPackage);
1015  }
1016  return null;
1017  }
1018 
1019  ConstructorInfo constructor;
1020  try
1021  {
1022  if (type != typeof(ItemComponent) && !type.IsSubclassOf(typeof(ItemComponent))) { return null; }
1023  constructor = type.GetConstructor(new Type[] { typeof(Item), typeof(ContentXElement) });
1024  if (constructor == null)
1025  {
1026  DebugConsole.ThrowError(
1027  $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})",
1028  contentPackage: element.ContentPackage);
1029  return null;
1030  }
1031  }
1032  catch (Exception e)
1033  {
1034  DebugConsole.ThrowError(
1035  $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e,
1036  contentPackage: element.ContentPackage);
1037  return null;
1038  }
1039  ItemComponent ic = null;
1040  try
1041  {
1042  object[] lobject = new object[] { item, element };
1043  object component = constructor.Invoke(lobject);
1044  ic = (ItemComponent)component;
1045  ic.name = element.Name.ToString();
1046  }
1047  catch (TargetInvocationException e)
1048  {
1049  DebugConsole.ThrowError($"Error while loading component of the type {type}.", e.InnerException, contentPackage: element.ContentPackage);
1050  GameAnalyticsManager.AddErrorEventOnce(
1051  $"ItemComponent.Load:TargetInvocationException{item.Name}{element.Name}",
1052  GameAnalyticsManager.ErrorSeverity.Error,
1053  $"Error while loading entity of the type {type} ({e.InnerException})\n{Environment.StackTrace.CleanupStackTrace()}");
1054  }
1055 
1056  return ic;
1057  }
1058 
1059  public virtual XElement Save(XElement parentElement)
1060  {
1061  XElement componentElement = new XElement(name);
1062 
1063  foreach (var kvp in RequiredItems)
1064  {
1065  foreach (RelatedItem ri in kvp.Value)
1066  {
1067  XElement newElement = new XElement("requireditem");
1068  ri.Save(newElement);
1069  componentElement.Add(newElement);
1070  }
1071  }
1072  foreach (RelatedItem ri in DisabledRequiredItems)
1073  {
1074  XElement newElement = new XElement("requireditem");
1075  ri.Save(newElement);
1076  componentElement.Add(newElement);
1077  }
1078 
1079 
1080  SerializableProperty.SerializeProperties(this, componentElement);
1081 
1082  parentElement.Add(componentElement);
1083  return componentElement;
1084  }
1085 
1086  public virtual void Reset()
1087  {
1089  if (this is Pickable) { canBePicked = true; }
1090  ParseMsg();
1091  OverrideRequiredItems(originalElement);
1092  }
1093 
1094  private void OverrideRequiredItems(ContentXElement element)
1095  {
1096  var prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(RequiredItems);
1097  RequiredItems.Clear();
1098 
1099  bool returnEmptyRequirements = false;
1100 #if CLIENT
1101  returnEmptyRequirements = Screen.Selected == GameMain.SubEditorScreen;
1102 #endif
1103  foreach (var subElement in element.Elements())
1104  {
1105  switch (subElement.Name.ToString().ToLowerInvariant())
1106  {
1107  case "requireditem":
1108  case "requireditems":
1109  RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmptyRequirements, item.Name);
1110  if (newRequiredItem == null) continue;
1111 
1112  var prevRequiredItem = prevRequiredItems.ContainsKey(newRequiredItem.Type) ?
1113  prevRequiredItems[newRequiredItem.Type].Find(ri => ri.JoinedIdentifiers == newRequiredItem.JoinedIdentifiers) : null;
1114  if (prevRequiredItem != null)
1115  {
1116  newRequiredItem.StatusEffects = prevRequiredItem.StatusEffects;
1117  newRequiredItem.Msg = prevRequiredItem.Msg;
1118  newRequiredItem.IsOptional = prevRequiredItem.IsOptional;
1119  newRequiredItem.IgnoreInEditor = prevRequiredItem.IgnoreInEditor;
1120  }
1121 
1122  if (!RequiredItems.ContainsKey(newRequiredItem.Type))
1123  {
1124  RequiredItems[newRequiredItem.Type] = new List<RelatedItem>();
1125  }
1126  RequiredItems[newRequiredItem.Type].Add(newRequiredItem);
1127  break;
1128  }
1129  }
1130  }
1131 
1132  public virtual void ParseMsg()
1133  {
1134  LocalizedString msg = TextManager.Get(Msg);
1135  if (msg.Loaded)
1136  {
1137  msg = TextManager.ParseInputTypes(msg);
1138  DisplayMsg = msg;
1139  }
1140  else
1141  {
1142  DisplayMsg = Msg;
1143  }
1144  }
1145 
1146  public interface IEventData { }
1147 
1148  public virtual bool ValidateEventData(NetEntityEvent.IData data)
1149  => true;
1150 
1152  => TryExtractEventData(data, out T componentData)
1153  ? componentData
1154  : throw new Exception($"Malformed item component state event for {item.Name} " +
1155  $"(item ID {item.ID}, component type {GetType().Name}): " +
1156  $"could not extract ComponentData of type {typeof(T).Name}");
1157 
1158  protected bool TryExtractEventData<T>(NetEntityEvent.IData data, out T componentData)
1159  {
1160  componentData = default;
1161  if (data is Item.ComponentStateEventData { ComponentData: T nestedData })
1162  {
1163  componentData = nestedData;
1164  return true;
1165  }
1166 
1167  return false;
1168  }
1169 
1170  #region AI related
1171  protected const float AIUpdateInterval = 0.2f;
1172  protected float aiUpdateTimer;
1173 
1174  protected AIObjectiveContainItem AIContainItems<T>(ItemContainer container, Character character, AIObjective currentObjective, int itemCount, bool equip, bool removeEmpty, bool spawnItemIfNotFound = false, bool dropItemOnDeselected = false) where T : ItemComponent
1175  {
1176  AIObjectiveContainItem containObjective = null;
1177  if (character.AIController is HumanAIController aiController)
1178  {
1179  containObjective = new AIObjectiveContainItem(character, container.ContainableItemIdentifiers, container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound)
1180  {
1181  ItemCount = itemCount,
1182  Equip = equip,
1183  RemoveEmpty = removeEmpty,
1184  GetItemPriority = i =>
1185  {
1186  if (i.ParentInventory?.Owner is Item)
1187  {
1188  //don't take items from other items of the same type
1189  if (((Item)i.ParentInventory.Owner).GetComponent<T>() != null)
1190  {
1191  return 0.0f;
1192  }
1193  }
1194  // Prefer items with the same identifier as the contained items'
1195  return container.ContainsItemsWithSameIdentifier(i) ? 1.0f : 0.5f;
1196  }
1197  };
1198  containObjective.Abandoned += () => aiController.IgnoredItems.Add(container.Item);
1199  if (dropItemOnDeselected)
1200  {
1201  currentObjective.Deselected += () =>
1202  {
1203  if (containObjective == null) { return; }
1204  if (containObjective.IsCompleted) { return; }
1205  Item item = containObjective.ItemToContain;
1206  if (item != null && character.CanInteractWith(item, checkLinked: false))
1207  {
1208  item.Drop(character);
1209  }
1210  };
1211  }
1212  currentObjective.AddSubObjective(containObjective);
1213  }
1214  return containObjective;
1215  }
1216  #endregion
1217  }
1218 }
void AddSubObjective(AIObjective objective, bool addFirst=false)
Action Deselected
A single shot event. Automatically cleared after launching. Use OnDeselected method for implementing ...
Action Abandoned
A single shot event. Automatically cleared after launching. Use OnAbandoned method for implementing (...
float GetSkillLevel(string skillIdentifier)
bool CanInteractWith(Character c, float maxDist=200.0f, bool checkVisibility=true, bool skipDistanceCheck=false)
IEnumerable< Item >?? HeldItems
Items the character has in their hand slots. Doesn't return nulls and only returns items held in both...
readonly? ContentPackage ContentPackage
Definition: ContentPath.cs:21
string? GetAttributeString(string key, string? def)
IEnumerable< XAttribute > Attributes()
ContentPackage? ContentPackage
Identifier NameAsIdentifier()
IEnumerable< ContentXElement > Elements()
static EntitySpawner Spawner
Definition: Entity.cs:31
Submarine Submarine
Definition: Entity.cs:53
RectTransform RectTransform
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static NetworkMember NetworkMember
Definition: GameMain.cs:190
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
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 DisableDrawableComponent(IDrawableComponent drawable)
void EnableDrawableComponent(IDrawableComponent drawable)
void Use(float deltaTime, Character user=null, Limb targetLimb=null, Entity useTarget=null, Character userForOnUsedEvent=null)
bool IsInteractable(Character character)
Returns interactibility based on whether the character is on a player team
bool IgnoreByAI(Character character)
void AddComponent(ItemComponent component)
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
float? Speed
Can be used by status effects or conditionals to the speed of the item
The base class for components holding the different functionalities of the item
bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg=null)
virtual bool ValidateEventData(NetEntityEvent.IData data)
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...
bool HasRequiredSkills(Character character, out Skill insufficientSkill)
int ManuallySelectedSound
Which sound should be played when manual sound selection type is selected? Not [Editable] because we ...
void ShallowRemove()
Remove the component so that it doesn't appear to exist in the game world (stop sounds,...
bool IsEmpty(Character user)
Returns true if the item is lacking required contained items, or if there's nothing with a non-zero c...
virtual bool HasAccess(Character character)
Only checks if any of the Picked requirements are matched (used for checking id card(s))....
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
string GetTextureDirectory(ContentXElement subElement)
virtual bool DontTransferInventoryBetweenSubs
If enabled, the contents of the item are not transferred when the player transfers items between subs...
virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
float Speed
Can be used by status effects or conditionals to the speed of the item
AIObjectiveContainItem AIContainItems< T >(ItemContainer container, Character character, AIObjective currentObjective, int itemCount, bool equip, bool removeEmpty, bool spawnItemIfNotFound=false, bool dropItemOnDeselected=false)
float DegreeOfSuccess(Character character)
Returns 0.0f-1.0f based on how well the Character can use the itemcomponent
virtual bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
true if the operation was completed
float DegreeOfSuccess(Character character, List< Skill > requiredSkills)
Returns 0.0f-1.0f based on how well the Character can use the itemcomponent
virtual bool DisallowSellingItemsFromContainer
If enabled, the items inside any of the item containers on this item cannot be sold at an outpost....
Dictionary< RelatedItem.RelationType, List< RelatedItem > > RequiredItems
virtual bool Pick(Character picker)
a Character has picked the item
virtual void OnInventoryChanged()
Called when the item has an ItemContainer and the contents inside of it changed.
static ItemComponent Load(ContentXElement element, Item item, bool errorMessages=true)
ContentPath FilePath
Definition: Prefab.cs:38
Conditionals are used by some in-game mechanics to require one or more conditions to be met for those...
static IEnumerable< PropertyConditional > FromXElement(ContentXElement element, Predicate< XAttribute >? predicate=null)
Point ScreenSpaceOffset
Screen space offset. From top left corner. In pixels.
RectTransform?? Parent
static Dictionary< Identifier, SerializableProperty > DeserializeProperties(object obj, XElement element=null)
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)
readonly Identifier Identifier
Definition: Skill.cs:7
float Level
Definition: Skill.cs:19
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?
static StatusEffect Load(ContentXElement element, string parentDebugName)
Vector2 DrawSize
The extents of the sprites or other graphics this component needs to draw. Used to determine which it...
void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth=-1, Color? overrideColor=null)
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19