Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/Items/Item.cs
5 using FarseerPhysics;
6 using Microsoft.Xna.Framework;
7 using Microsoft.Xna.Framework.Graphics;
8 using Microsoft.Xna.Framework.Input;
9 using System;
10 using System.Collections.Generic;
11 using System.Collections.Immutable;
12 using System.Diagnostics.CodeAnalysis;
13 using System.Globalization;
14 using System.Linq;
15 using System.Text;
16 
17 namespace Barotrauma
18 {
19  partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable
20  {
21  public static bool ShowItems = true, ShowWires = true;
22 
23  private readonly List<PosInfo> positionBuffer = new List<PosInfo>();
24 
25  private readonly List<ItemComponent> activeHUDs = new List<ItemComponent>();
26 
27  private readonly List<SerializableEntityEditor> activeEditors = new List<SerializableEntityEditor>();
28 
29 
30  private GUIComponentStyle iconStyle;
32  {
33  get { return iconStyle; }
34  private set
35  {
36  if (IconStyle != value)
37  {
38  iconStyle = value;
40  }
41  }
42  }
43 
44  partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable<Client> targetClients)
45  {
46  if (interactionType == CampaignMode.InteractionType.None)
47  {
48  IconStyle = null;
49  }
50  else
51  {
52  IconStyle = GUIStyle.GetComponentStyle($"CampaignInteractionIcon.{interactionType}");
53  }
54  }
55 
56  public IEnumerable<ItemComponent> ActiveHUDs => activeHUDs;
57 
58  public float LastImpactSoundTime;
59  public const float ImpactSoundInterval = 0.2f;
60 
61  private float editingHUDRefreshTimer;
62 
63  private ContainedItemSprite activeContainedSprite;
64 
65  private readonly Dictionary<DecorativeSprite, DecorativeSprite.State> spriteAnimState = new Dictionary<DecorativeSprite, DecorativeSprite.State>();
66 
67  public float DrawDepthOffset;
68 
69  private bool fakeBroken;
70  public bool FakeBroken
71  {
72  get { return fakeBroken; }
73  set
74  {
75  if (value != fakeBroken)
76  {
77  fakeBroken = value;
79  }
80  }
81  }
82 
83  private Sprite activeSprite;
84  public override Sprite Sprite
85  {
86  get { return activeSprite; }
87  }
88 
89  public override Rectangle Rect
90  {
91  get { return base.Rect; }
92  set
93  {
94  cachedVisibleExtents = null;
95  base.Rect = value;
96  }
97  }
98 
99  public override bool DrawBelowWater => (!(Screen.Selected is SubEditorScreen editor) || !editor.WiringMode || !isWire || !isLogic) && (base.DrawBelowWater || ParentInventory is CharacterInventory);
100 
101  public override bool DrawOverWater => base.DrawOverWater || (IsSelected || Screen.Selected is SubEditorScreen editor && editor.WiringMode) && (isWire || isLogic);
102 
103  private GUITextBlock itemInUseWarning;
104  private GUITextBlock ItemInUseWarning
105  {
106  get
107  {
108  if (itemInUseWarning == null)
109  {
110  itemInUseWarning = new GUITextBlock(new RectTransform(new Point(10), GUI.Canvas), "",
111  textColor: GUIStyle.Orange, color: Color.Black,
112  textAlignment: Alignment.Center, style: "OuterGlow");
113  }
114  return itemInUseWarning;
115  }
116  }
117 
118  public override bool SelectableInEditor
119  {
120  get
121  {
123  {
124  return false;
125  }
126 
127  if (!SubEditorScreen.IsLayerVisible(this)) { return false;}
128 
129  return parentInventory == null && (body == null || body.Enabled) && ShowItems;
130  }
131  }
132 
133  public float GetDrawDepth()
134  {
136  }
137 
138  public Color GetSpriteColor(Color? defaultColor = null, bool withHighlight = false)
139  {
140  Color color = defaultColor ?? spriteColor;
141  if (Prefab.UseContainedSpriteColor && ownInventory != null)
142  {
143  foreach (Item item in ContainedItems)
144  {
145  color = item.ContainerColor;
146  break;
147  }
148  }
149  if (withHighlight)
150  {
151  if (IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen)
152  {
153  color = GUIStyle.Orange * Math.Max(GetSpriteColor().A / (float)byte.MaxValue, 0.1f);
154  }
155  else if (IsHighlighted && HighlightColor.HasValue)
156  {
157  color = Color.Lerp(color, HighlightColor.Value, (MathF.Sin((float)Timing.TotalTime * 3.0f) + 1.0f) / 2.0f);
158  }
159  }
160  return color;
161  }
162 
163  protected override void CheckIsHighlighted()
164  {
165  if (IsHighlighted || ExternalHighlight || IconStyle != null)
166  {
167  highlightedEntities.Add(this);
168  }
169  else
170  {
171  highlightedEntities.Remove(this);
172  }
173  }
174 
175  public Color GetInventoryIconColor()
176  {
177  Color color = InventoryIconColor;
178  if (Prefab.UseContainedInventoryIconColor && ownInventory != null)
179  {
180  foreach (Item item in ContainedItems)
181  {
182  color = item.ContainerColor;
183  break;
184  }
185  }
186  return color;
187  }
188 
189  partial void SetActiveSpriteProjSpecific()
190  {
191  activeSprite = Prefab.Sprite;
192  activeContainedSprite = null;
193  Holdable holdable = GetComponent<Holdable>();
194  if (holdable != null && holdable.Attached)
195  {
196  foreach (ContainedItemSprite containedSprite in Prefab.ContainedSprites)
197  {
198  if (containedSprite.UseWhenAttached)
199  {
200  activeContainedSprite = containedSprite;
201  activeSprite = containedSprite.Sprite;
202  UpdateSpriteStates(0.0f);
203  return;
204  }
205  }
206  }
207 
208  if (Container != null)
209  {
210  foreach (ContainedItemSprite containedSprite in Prefab.ContainedSprites)
211  {
212  if (containedSprite.MatchesContainer(Container))
213  {
214  activeContainedSprite = containedSprite;
215  activeSprite = containedSprite.Sprite;
216  UpdateSpriteStates(0.0f);
217  return;
218  }
219  }
220  }
221 
222  float displayCondition = FakeBroken ? 0.0f : ConditionPercentageRelativeToDefaultMaxCondition;
223  for (int i = 0; i < Prefab.BrokenSprites.Length;i++)
224  {
225  if (Prefab.BrokenSprites[i].FadeIn) { continue; }
226  float minCondition = i > 0 ? Prefab.BrokenSprites[i - i].MaxConditionPercentage : 0.0f;
227  if (displayCondition <= minCondition || displayCondition <= Prefab.BrokenSprites[i].MaxConditionPercentage)
228  {
229  activeSprite = Prefab.BrokenSprites[i].Sprite;
230  break;
231  }
232  }
233  }
234 
235  public void InitSpriteStates()
236  {
237  Prefab.Sprite?.EnsureLazyLoaded();
238  Prefab.InventoryIcon?.EnsureLazyLoaded();
239  foreach (BrokenItemSprite brokenSprite in Prefab.BrokenSprites)
240  {
241  brokenSprite.Sprite.EnsureLazyLoaded();
242  }
243  foreach (var decorativeSprite in Prefab.DecorativeSprites)
244  {
245  decorativeSprite.Sprite.EnsureLazyLoaded();
246  spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State());
247  }
248  SetActiveSprite();
249  UpdateSpriteStates(0.0f);
250  }
251 
252  partial void InitProjSpecific()
253  {
255  }
256 
257  private Rectangle? cachedVisibleExtents;
258 
260  {
261  cachedVisibleExtents = null;
262  }
263 
264  public override bool IsVisible(Rectangle worldView)
265  {
266  // Inside of a container
267  if (container != null)
268  {
269  return false;
270  }
271 
272  //no drawable components and the body has been disabled = nothing to draw
273  if (!hasComponentsToDraw && body != null && !body.Enabled)
274  {
275  return false;
276  }
277 
278  if (parentInventory?.Owner is Character character && character.InvisibleTimer > 0.0f)
279  {
280  return false;
281  }
282 
283  Rectangle extents;
284  if (cachedVisibleExtents.HasValue)
285  {
286  extents = cachedVisibleExtents.Value;
287  }
288  else
289  {
290  int padding = 100;
291 
292  RectangleF boundingBox = GetTransformedQuad().BoundingAxisAlignedRectangle;
293  Vector2 min = new Vector2(-boundingBox.Width / 2 - padding, -boundingBox.Height / 2 - padding);
294  Vector2 max = -min;
295 
296  foreach (IDrawableComponent drawable in drawableComponents)
297  {
298  min.X = Math.Min(min.X, -drawable.DrawSize.X / 2);
299  min.Y = Math.Min(min.Y, -drawable.DrawSize.Y / 2);
300  max.X = Math.Max(max.X, drawable.DrawSize.X / 2);
301  max.Y = Math.Max(max.Y, drawable.DrawSize.Y / 2);
302  }
303  foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites)
304  {
305  float scale = decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale;
306  min.X = Math.Min(-decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * scale, min.X);
307  min.Y = Math.Min(-decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale, min.Y);
308  max.X = Math.Max(decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * scale, max.X);
309  max.Y = Math.Max(decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale, max.Y);
310  }
311  cachedVisibleExtents = extents = new Rectangle(min.ToPoint(), max.ToPoint());
312  }
313 
314  Vector2 worldPosition = WorldPosition + GetCollapseEffectOffset();
315 
316  if (worldPosition.X + extents.X > worldView.Right || worldPosition.X + extents.Width < worldView.X) { return false; }
317  if (worldPosition.Y + extents.Height < worldView.Y - worldView.Height || worldPosition.Y + extents.Y > worldView.Y) { return false; }
318 
319  return true;
320  }
321 
322  public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true)
323  {
324  Draw(spriteBatch, editing, back, overrideColor: null);
325  }
326 
327  public void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Color? overrideColor = null)
328  {
329  if (!Visible || (!editing && IsHidden) || !SubEditorScreen.IsLayerVisible(this)) { return; }
330 
331  if (editing)
332  {
333  if (isWire)
334  {
335  if (!ShowWires) { return; }
336  }
337  else if (!ShowItems) { return; }
338  }
339 
340  Color color = GetSpriteColor(spriteColor);
341 
342  bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null;
343  bool renderTransparent = isWiringMode && GetComponent<ConnectionPanel>() == null;
344  if (renderTransparent) { color *= 0.15f; }
345 
347  {
348  color = Color.Red;
349  foreach (var ic in components)
350  {
351  var interactionType = GetComponentInteractionVisibility(Character.Controlled, ic);
352  if (interactionType == InteractionVisibility.MissingRequirement)
353  {
354  color = Color.Orange;
355  }
356  else if (interactionType == InteractionVisibility.Visible)
357  {
358  color = Color.LightGreen;
359  break;
360  }
361  }
362  }
363 
364  BrokenItemSprite fadeInBrokenSprite = null;
365  float fadeInBrokenSpriteAlpha = 0.0f;
366 
367  float displayCondition = FakeBroken ? 0.0f : ConditionPercentageRelativeToDefaultMaxCondition;
368  Vector2 drawOffset = GetCollapseEffectOffset();
369  drawOffset.Y = -drawOffset.Y;
370 
371  if (displayCondition < MaxCondition)
372  {
373  for (int i = 0; i < Prefab.BrokenSprites.Length; i++)
374  {
375  if (Prefab.BrokenSprites[i].FadeIn)
376  {
377  float min = i > 0 ? Prefab.BrokenSprites[i - i].MaxConditionPercentage : 0.0f;
378  float max = Prefab.BrokenSprites[i].MaxConditionPercentage;
379  fadeInBrokenSpriteAlpha = 1.0f - ((displayCondition - min) / (max - min));
380  if (fadeInBrokenSpriteAlpha > 0.0f && fadeInBrokenSpriteAlpha <= 1.0f)
381  {
382  fadeInBrokenSprite = Prefab.BrokenSprites[i];
383  }
384  continue;
385  }
386  if (displayCondition <= Prefab.BrokenSprites[i].MaxConditionPercentage)
387  {
388  activeSprite = Prefab.BrokenSprites[i].Sprite;
389  drawOffset = Prefab.BrokenSprites[i].Offset.ToVector2() * Scale;
390  break;
391  }
392  }
393  }
394 
395  float depth = GetDrawDepth();
396  if (isWiringMode && isLogic && !PlayerInput.IsShiftDown()) { depth = 0.01f; }
397  if (activeSprite != null)
398  {
399  SpriteEffects oldEffects = activeSprite.effects;
400  activeSprite.effects ^= SpriteEffects;
401  SpriteEffects oldBrokenSpriteEffects = SpriteEffects.None;
402  if (fadeInBrokenSprite != null && fadeInBrokenSprite.Sprite != activeSprite)
403  {
404  oldBrokenSpriteEffects = fadeInBrokenSprite.Sprite.effects;
405  fadeInBrokenSprite.Sprite.effects ^= SpriteEffects;
406  }
407 
408  if (body == null)
409  {
410  if (Prefab.ResizeHorizontal || Prefab.ResizeVertical)
411  {
412  if (color.A > 0)
413  {
414  Vector2 size = new Vector2(rect.Width, rect.Height);
415  activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset,
416  size, color: color,
417  textureScale: Vector2.One * Scale,
418  depth: depth);
419 
420  if (fadeInBrokenSprite != null)
421  {
422  float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f);
423  fadeInBrokenSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha,
424  textureScale: Vector2.One * Scale,
425  depth: d);
426  }
427  DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth, overrideColor);
428  }
429  }
430  else
431  {
432  Vector2 origin = GetSpriteOrigin(activeSprite);
433  if (color.A > 0)
434  {
435  activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, RotationRad, Scale, activeSprite.effects, depth);
436  if (fadeInBrokenSprite != null)
437  {
438  float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f);
439  fadeInBrokenSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, RotationRad, Scale, activeSprite.effects, d);
440  }
441  }
442  if (Infector != null && (Infector.ParentBallastFlora.HasBrokenThrough || BallastFloraBehavior.AlwaysShowBallastFloraSprite))
443  {
444  Prefab.InfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, Prefab.InfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.001f);
445  Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.002f);
446  }
447 
448  DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, -RotationRad, depth, overrideColor);
449  }
450  }
451  else if (body.Enabled)
452  {
453  var holdable = GetComponent<Holdable>();
454  if (holdable != null && holdable.Picker?.AnimController != null)
455  {
456  //don't draw the item on hands if it's also being worn
457  if (GetComponent<Wearable>() is { IsActive: true }) { return; }
458  if (!back) { return; }
459  if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == this)
460  {
461  depth = GetHeldItemDepth(LimbType.RightHand, holdable, depth);
462  }
463  else if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == this)
464  {
465  depth = GetHeldItemDepth(LimbType.LeftHand, holdable, depth);
466  }
467 
468  static float GetHeldItemDepth(LimbType limb, Holdable holdable, float depth)
469  {
470  if (holdable?.Picker?.AnimController == null) { return depth; }
471  //offset used to make sure the item draws just slightly behind the right hand, or slightly in front of the left hand
472  float limbDepthOffset = 0.000001f;
473  float depthOffset = holdable.Picker.AnimController.GetDepthOffset();
474  //use the upper arm as a reference, to ensure the item gets drawn behind / in front of the whole arm (not just the forearm)
475  Limb holdLimb = holdable.Picker.AnimController.GetLimb(limb == LimbType.RightHand ? LimbType.RightArm : LimbType.LeftArm);
476  if (holdLimb?.ActiveSprite != null)
477  {
478  depth =
479  holdLimb.ActiveSprite.Depth
480  + depthOffset
481  + limbDepthOffset * 2 * (limb == LimbType.RightHand ? 1 : -1);
482  foreach (WearableSprite wearableSprite in holdLimb.WearingItems)
483  {
484  if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null)
485  {
486  depth =
487  limb == LimbType.RightHand ?
488  Math.Max(wearableSprite.Sprite.Depth + limbDepthOffset, depth) :
489  Math.Min(wearableSprite.Sprite.Depth - limbDepthOffset, depth);
490  }
491  }
492  var head = holdable.Picker.AnimController.GetLimb(LimbType.Head);
493  if (head != null)
494  {
495  //ensure the holdable item is always drawn in front of the head no matter what the wearables or whatnot do with the sprite depths
496  depth =
497  limb == LimbType.RightHand ?
498  Math.Min(head.Sprite.Depth + depthOffset - limbDepthOffset, depth) :
499  Math.Max(head.Sprite.Depth + depthOffset + limbDepthOffset, depth);
500  }
501  }
502  return depth;
503  }
504  }
505  Vector2 origin = GetSpriteOrigin(activeSprite);
506  body.Draw(spriteBatch, activeSprite, color, depth, Scale, origin: origin);
507  if (fadeInBrokenSprite != null)
508  {
509  float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f);
510  body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, d, Scale);
511  }
512  DrawDecorativeSprites(spriteBatch, body.DrawPosition, flipX: body.Dir < 0, flipY: false, rotation: body.Rotation, depth, overrideColor);
513  }
514 
515  foreach (var upgrade in Upgrades)
516  {
517  var upgradeSprites = GetUpgradeSprites(upgrade);
518 
519  foreach (var decorativeSprite in upgradeSprites)
520  {
521  if (!spriteAnimState[decorativeSprite].IsActive) { continue; }
522  float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor);
523  Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale;
524  if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; }
525  if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; }
526  decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color,
527  rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects,
528  depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth));
529  }
530  }
531 
532  activeSprite.effects = oldEffects;
533  if (fadeInBrokenSprite != null && fadeInBrokenSprite.Sprite != activeSprite)
534  {
535  fadeInBrokenSprite.Sprite.effects = oldBrokenSpriteEffects;
536  }
537  }
538 
539  //use a backwards for loop because the drawable components may disable drawing,
540  //causing them to be removed from the list
541  for (int i = drawableComponents.Count - 1; i >= 0; i--)
542  {
543  drawableComponents[i].Draw(spriteBatch, editing, depth, overrideColor);
544  }
545 
546  if (GameMain.DebugDraw)
547  {
548  body?.DebugDraw(spriteBatch, Color.White);
549  if (GetComponent<TriggerComponent>()?.PhysicsBody is PhysicsBody triggerBody)
550  {
551  triggerBody.UpdateDrawPosition();
552  triggerBody.DebugDraw(spriteBatch, Color.White);
553  }
554  }
555 
556  if (editing && IsSelected && PlayerInput.KeyDown(Keys.Space))
557  {
558  if (GetComponent<ElectricalDischarger>() is { } discharger)
559  {
560  discharger.DrawElectricity(spriteBatch);
561  }
562  }
563 
564  if (!editing || (body != null && !body.Enabled))
565  {
566  return;
567  }
568 
569  if (IsSelected || IsHighlighted)
570  {
571  Vector2 drawPos = new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2));
572  Vector2 drawSize = new Vector2(MathF.Ceiling(rect.Width + Math.Abs(drawPos.X - (int)drawPos.X)), MathF.Ceiling(rect.Height + Math.Abs(drawPos.Y - (int)drawPos.Y)));
573  drawPos = new Vector2(MathF.Floor(drawPos.X), MathF.Floor(drawPos.Y));
574  GUI.DrawRectangle(sb: spriteBatch,
575  center: drawPos + drawSize * 0.5f,
576  width: drawSize.X,
577  height: drawSize.Y,
578  rotation: RotationRad,
579  clr: Color.White,
580  depth: 0,
581  thickness: Math.Max(2f / Screen.Selected.Cam.Zoom, 1));
582 
583  foreach (Rectangle t in Prefab.Triggers)
584  {
585  Rectangle transformedTrigger = TransformTrigger(t);
586 
587  Vector2 rectWorldPos = new Vector2(transformedTrigger.X, transformedTrigger.Y);
588  if (Submarine != null) rectWorldPos += Submarine.Position;
589  rectWorldPos.Y = -rectWorldPos.Y;
590 
591  GUI.DrawRectangle(spriteBatch,
592  rectWorldPos,
593  new Vector2(transformedTrigger.Width, transformedTrigger.Height),
594  GUIStyle.Green,
595  false,
596  0,
597  (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f));
598  }
599  }
600 
601  if (!ShowLinks || GUI.DisableHUD) { return; }
602 
603  foreach (MapEntity e in linkedTo)
604  {
605  bool isLinkAllowed = Prefab.IsLinkAllowed(e.Prefab);
606  Color lineColor = GUIStyle.Red * 0.5f;
607  if (isLinkAllowed)
608  {
609  lineColor = e is Item i && (DisplaySideBySideWhenLinked || i.DisplaySideBySideWhenLinked) ? Color.Purple * 0.5f : Color.LightGreen * 0.5f;
610  }
611  Vector2 from = new Vector2(WorldPosition.X, -WorldPosition.Y);
612  Vector2 to = new Vector2(e.WorldPosition.X, -e.WorldPosition.Y);
613  GUI.DrawLine(spriteBatch, from, to, lineColor * 0.25f, width: 3);
614  GUI.DrawLine(spriteBatch, from, to, lineColor, width: 1);
615  //GUI.DrawString(spriteBatch, from, $"Linked to {e.Name}", lineColor, Color.Black * 0.5f);
616  }
617 
618  Vector2 GetSpriteOrigin(Sprite sprite)
619  {
620  Vector2 origin = sprite.Origin;
621  if ((sprite.effects & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally)
622  {
623  origin.X = sprite.SourceRect.Width - origin.X;
624  }
625  if ((sprite.effects & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically)
626  {
627  origin.Y = sprite.SourceRect.Height - origin.Y;
628  }
629  return origin;
630  }
631 
632  Color GetSpriteColor(Color defaultColor)
633  {
634  return
635  overrideColor ??
636  (IsIncludedInSelection && editing ? GUIStyle.Blue : this.GetSpriteColor(defaultColor: defaultColor, withHighlight: true));
637  }
638  }
639 
640  public void DrawDecorativeSprites(SpriteBatch spriteBatch, Vector2 drawPos, bool flipX, bool flipY, float rotation, float depth, Color? overrideColor = null)
641  {
642  foreach (var decorativeSprite in Prefab.DecorativeSprites)
643  {
644  Color decorativeSpriteColor = overrideColor ?? GetSpriteColor(decorativeSprite.Color).Multiply(GetSpriteColor(spriteColor));
645  if (!spriteAnimState[decorativeSprite].IsActive) { continue; }
646 
647  Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier,
648  flipX ^ flipY ? -rotation : rotation) * Scale;
649 
651  {
652  decorativeSprite.Sprite.DrawTiled(spriteBatch,
653  new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)),
654  new Vector2(rect.Width, rect.Height), color: decorativeSpriteColor,
655  textureScale: Vector2.One * Scale,
656  depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f));
657  }
658  else
659  {
660  float spriteRotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor);
661 
662  Vector2 origin = decorativeSprite.Sprite.Origin;
663  SpriteEffects spriteEffects = SpriteEffects.None;
664  if (flipX && Prefab.CanSpriteFlipX)
665  {
666  offset.X = -offset.X;
667  origin.X = -origin.X + decorativeSprite.Sprite.size.X;
668  spriteEffects = SpriteEffects.FlipHorizontally;
669  }
670  if (flipY && Prefab.CanSpriteFlipY)
671  {
672  offset.Y = -offset.Y;
673  origin.Y = -origin.Y + decorativeSprite.Sprite.size.Y;
674  spriteEffects |= SpriteEffects.FlipVertically;
675  }
676  if (body != null)
677  {
678  var ca = MathF.Cos(-body.DrawRotation);
679  var sa = MathF.Sin(-body.DrawRotation);
680  offset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y);
681  }
682  decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(drawPos.X + offset.X, -(drawPos.Y + offset.Y)), decorativeSpriteColor, origin,
683  -rotation + spriteRotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffects,
684  depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth));
685  }
686  }
687  }
688 
689  partial void OnCollisionProjSpecific(float impact)
690  {
691  if (impact > 1.0f &&
692  Container == null &&
693  !string.IsNullOrEmpty(Prefab.ImpactSoundTag) &&
694  Timing.TotalTime > LastImpactSoundTime + ImpactSoundInterval)
695  {
696  LastImpactSoundTime = (float)Timing.TotalTime;
697  SoundPlayer.PlaySound(Prefab.ImpactSoundTag, WorldPosition, hullGuess: CurrentHull);
698  }
699  }
700 
701  partial void Splash()
702  {
703  if (body == null || CurrentHull == null) { return; }
704  //create a splash particle
705  float massFactor = MathHelper.Clamp(body.Mass, 0.5f, 20.0f);
706  for (int i = 0; i < MathHelper.Clamp(Math.Abs(body.LinearVelocity.Y), 1.0f, 10.0f); i++)
707  {
708  var splash = GameMain.ParticleManager.CreateParticle("watersplash",
709  new Vector2(WorldPosition.X, CurrentHull.WorldSurface),
710  new Vector2(0.0f, Math.Abs(-body.LinearVelocity.Y * massFactor)) + Rand.Vector(Math.Abs(body.LinearVelocity.Y * 10)),
711  Rand.Range(0.0f, MathHelper.TwoPi), CurrentHull);
712  if (splash != null)
713  {
714  splash.Size *= MathHelper.Clamp(Math.Abs(body.LinearVelocity.Y) * 0.1f * massFactor, 1.0f, 4.0f);
715  }
716  }
717  GameMain.ParticleManager.CreateParticle("bubbles",
718  new Vector2(WorldPosition.X, CurrentHull.WorldSurface),
719  body.LinearVelocity * massFactor,
720  0.0f, CurrentHull);
721 
722  //create a wave
723  if (body.LinearVelocity.Y < 0.0f)
724  {
725  int n = (int)((Position.X - CurrentHull.Rect.X) / Hull.WaveWidth);
726  if (n >= 0 && n < currentHull.WaveVel.Length)
727  {
728  CurrentHull.WaveVel[n] += MathHelper.Clamp(body.LinearVelocity.Y * massFactor, -5.0f, 5.0f);
729  }
730  }
731  SoundPlayer.PlaySplashSound(WorldPosition, Math.Abs(body.LinearVelocity.Y) + Rand.Range(-10.0f, -5.0f));
732  }
733 
735  {
736  if (ic.NeedsSoundUpdate())
737  {
738  if (!updateableComponents.Contains(ic))
739  {
740  updateableComponents.Add(ic);
741  }
742  isActive = true;
743  }
744  }
745 
746  public void UpdateSpriteStates(float deltaTime)
747  {
748  if (activeContainedSprite != null)
749  {
750  if (activeContainedSprite.DecorativeSpriteBehavior == ContainedItemSprite.DecorativeSpriteBehaviorType.HideWhenVisible)
751  {
752  foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites)
753  {
754  var spriteState = spriteAnimState[decorativeSprite];
755  spriteState.IsActive = false;
756  }
757  return;
758  }
759  }
760  else
761  {
762  foreach (var containedSprite in Prefab.ContainedSprites)
763  {
764  if (containedSprite.Sprite != activeSprite && containedSprite.DecorativeSpriteBehavior == ContainedItemSprite.DecorativeSpriteBehaviorType.HideWhenNotVisible)
765  {
766  foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites)
767  {
768  var spriteState = spriteAnimState[decorativeSprite];
769  spriteState.IsActive = false;
770  }
771  return;
772  }
773  }
774  }
775 
776  if (Prefab.DecorativeSpriteGroups.Count > 0)
777  {
778  DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches);
779  }
780 
781  foreach (var upgrade in Upgrades)
782  {
783  var upgradeSprites = GetUpgradeSprites(upgrade);
784  foreach (var decorativeSprite in upgradeSprites)
785  {
786  var spriteState = spriteAnimState[decorativeSprite];
787  spriteState.IsActive = true;
788  foreach (var conditional in decorativeSprite.IsActiveConditionals)
789  {
790  if (!ConditionalMatches(conditional))
791  {
792  spriteState.IsActive = false;
793  break;
794  }
795  }
796  }
797  }
798  }
799 
800  public override void UpdateEditing(Camera cam, float deltaTime)
801  {
802  if (editingHUD == null || editingHUD.UserData as Item != this)
803  {
805  editingHUDRefreshTimer = 1.0f;
806  }
807  if (editingHUDRefreshTimer <= 0.0f)
808  {
809  activeEditors.ForEach(e => e?.RefreshValues());
810  editingHUDRefreshTimer = 1.0f;
811  }
812 
813  if (Screen.Selected != GameMain.SubEditorScreen) { return; }
814 
815  if (GetComponent<ElectricalDischarger>() is { } discharger)
816  {
817  if (PlayerInput.KeyDown(Keys.Space))
818  {
819  discharger.FindNodes(WorldPosition, discharger.Range);
820  }
821  else
822  {
823  discharger.IsActive = false;
824  }
825  }
826 
827  if (Character.Controlled == null) { activeHUDs.Clear(); }
828 
829  foreach (ItemComponent ic in components)
830  {
831  ic.UpdateEditing(deltaTime);
832  }
833 
834  if (!Linkable) { return; }
835 
836  if (!PlayerInput.KeyDown(Keys.Space)) { return; }
837  bool lClick = PlayerInput.PrimaryMouseButtonClicked();
839  if (!lClick && !rClick) { return; }
840 
841  Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition);
842  var otherEntity = highlightedEntities.FirstOrDefault(e => e != this && e.IsMouseOn(position));
843  if (otherEntity != null)
844  {
845  if (linkedTo.Contains(otherEntity))
846  {
847  linkedTo.Remove(otherEntity);
848  if (otherEntity.linkedTo != null && otherEntity.linkedTo.Contains(this))
849  {
850  otherEntity.linkedTo.Remove(this);
851  }
852  }
853  else
854  {
855  linkedTo.Add(otherEntity);
856  if (otherEntity.Linkable && otherEntity.linkedTo != null)
857  {
858  otherEntity.linkedTo.Add(this);
859  }
860  }
861  }
862  }
863 
864  public override bool IsMouseOn(Vector2 position)
865  {
866  Vector2 rectSize = rect.Size.ToVector2();
867 
868  Vector2 bodyPos = WorldPosition;
869 
870  Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, RotationRad);
871 
872  return
873  Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f &&
874  Math.Abs(transformedMousePos.Y - bodyPos.Y) < rectSize.Y / 2.0f;
875  }
876 
877  public GUIComponent CreateEditingHUD(bool inGame = false)
878  {
879  activeEditors.Clear();
880 
881  int heightScaled = (int)(20 * GUI.Scale);
882  editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this };
883  GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null)
884  {
885  CanTakeKeyBoardFocus = false,
886  Spacing = (int)(25 * GUI.Scale)
887  };
888 
889  var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this };
890  activeEditors.Add(itemEditor);
891  itemEditor.Children.First().Color = Color.Black * 0.7f;
892  if (!inGame)
893  {
894  if (Linkable)
895  {
896  var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("HoldToLink"), font: GUIStyle.SmallFont);
897  var itemsText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("AllowedLinks"), font: GUIStyle.SmallFont);
898  LocalizedString allowedItems = AllowedLinks.None() ? TextManager.Get("None") : string.Join(", ", AllowedLinks);
899  itemsText.Text = TextManager.AddPunctuation(':', itemsText.Text, allowedItems);
900  itemEditor.AddCustomContent(linkText, 1);
901  itemEditor.AddCustomContent(itemsText, 2);
902  linkText.TextColor = GUIStyle.Orange;
903  itemsText.TextColor = GUIStyle.Orange;
904  }
905 
906  //create a tag picker for item containers to make it easier to pick relevant tags for PreferredContainers
907  var itemContainer = GetComponent<ItemContainer>();
908  if (itemContainer != null)
909  {
910  var tagBox = itemEditor.Fields["Tags".ToIdentifier()].First() as GUITextBox;
911  var tagsField = tagBox?.Parent;
912 
913  var containerTagLayout = new GUILayoutGroup(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), isHorizontal: true);
914  var containerTagButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1), containerTagLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight);
915  new GUIButton(new RectTransform(new Vector2(0.95f, 1), containerTagButtonLayout.RectTransform), text: TextManager.Get("containertaguibutton"), style: "GUIButtonSmall")
916  {
917  OnClicked = (_, _) => { CreateContainerTagPicker(tagBox); return true; },
918  TextBlock = { AutoScaleHorizontal = true }
919  };
920  var containerTagText = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1), containerTagLayout.RectTransform), TextManager.Get("containertaguibuttondescription"), font: GUIStyle.SmallFont)
921  {
922  TextColor = GUIStyle.Orange
923  };
924  var limitedString = ToolBox.LimitString(containerTagText.Text, containerTagText.Font, itemEditor.Rect.Width - containerTagButtonLayout.Rect.Width);
925  if (limitedString != containerTagText.Text)
926  {
927  containerTagText.ToolTip = containerTagText.Text;
928  containerTagText.Text = limitedString;
929  }
930  itemEditor.AddCustomContent(containerTagLayout, 3);
931  }
932 
933  var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), isHorizontal: true)
934  {
935  Stretch = true,
936  RelativeSpacing = 0.02f,
937  CanBeFocused = true
938  };
939 
940  GUINumberInput rotationField =
941  itemEditor.Fields.TryGetValue("Rotation".ToIdentifier(), out var rotationFieldComponents)
942  ? rotationFieldComponents.OfType<GUINumberInput>().FirstOrDefault()
943  : null;
944 
945  var mirrorX = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall")
946  {
947  ToolTip = TextManager.Get("MirrorEntityXToolTip"),
948  Enabled = Prefab.CanFlipX,
949  OnClicked = (button, data) =>
950  {
951  foreach (MapEntity me in SelectedList)
952  {
953  me.FlipX(relativeToSub: false);
954  }
955  if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); }
956  ColorFlipButton(button, FlippedX);
957  if (rotationField != null) { rotationField.FloatValue = Rotation; }
958  return true;
959  }
960  };
961  ColorFlipButton(mirrorX, FlippedX);
962  var mirrorY = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall")
963  {
964  ToolTip = TextManager.Get("MirrorEntityYToolTip"),
965  Enabled = Prefab.CanFlipY,
966  OnClicked = (button, data) =>
967  {
968  foreach (MapEntity me in SelectedList)
969  {
970  me.FlipY(relativeToSub: false);
971  }
972  if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); }
973  ColorFlipButton(button, FlippedY);
974  if (rotationField != null) { rotationField.FloatValue = Rotation; }
975  return true;
976  }
977  };
978  ColorFlipButton(mirrorY, FlippedY);
979  if (Sprite != null)
980  {
981  var reloadTextureButton = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite"), style: "GUIButtonSmall");
982  reloadTextureButton.OnClicked += (button, data) =>
983  {
984  Sprite.ReloadXML();
986  return true;
987  };
988  }
989  new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ResetToPrefab"), style: "GUIButtonSmall")
990  {
991  OnClicked = (button, data) =>
992  {
993  foreach (MapEntity me in SelectedList)
994  {
995  (me as Item)?.Reset();
996  (me as Structure)?.Reset();
997  }
998  if (!SelectedList.Contains(this)) { Reset(); }
1000  return true;
1001  }
1002  };
1003  buttonContainer.RectTransform.MinSize = new Point(0, buttonContainer.RectTransform.Children.Max(c => c.MinSize.Y));
1004  buttonContainer.RectTransform.IsFixedSize = true;
1005  itemEditor.AddCustomContent(buttonContainer, itemEditor.ContentCount);
1006  GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Select(b => ((GUIButton)b).TextBlock));
1007 
1008  if (Submarine.MainSub?.Info?.Type == SubmarineType.OutpostModule)
1009  {
1010  GUITickBox tickBox = new GUITickBox(new RectTransform(new Point(listBox.Content.Rect.Width, 10)), TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.name"))
1011  {
1012  Font = GUIStyle.SmallFont,
1014  ToolTip = TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.description"),
1015  OnSelected = (tickBox) =>
1016  {
1018  return true;
1019  }
1020  };
1021  itemEditor.AddCustomContent(tickBox, 1);
1022  }
1023 
1024  if (!Layer.IsNullOrEmpty())
1025  {
1026  var layerText = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)) { MinSize = new Point(0, heightScaled) }, TextManager.AddPunctuation(':', TextManager.Get("editor.layer"), Layer));
1027  itemEditor.AddCustomContent(layerText, 1);
1028  }
1029  }
1030 
1031  foreach (ItemComponent ic in components)
1032  {
1033  if (inGame)
1034  {
1035  if (!ic.AllowInGameEditing) { continue; }
1036  if (SerializableProperty.GetProperties<InGameEditable>(ic).Count == 0 &&
1038  {
1039  continue;
1040  }
1041  }
1042  else
1043  {
1044  if (ic.RequiredItems.Count == 0 && ic.DisabledRequiredItems.Count == 0 && SerializableProperty.GetProperties<Editable>(ic).Count == 0) { continue; }
1045  }
1046 
1047  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine");
1048 
1049  var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, titleFont: GUIStyle.SubHeadingFont) { UserData = ic };
1050  componentEditor.Children.First().Color = Color.Black * 0.7f;
1051  activeEditors.Add(componentEditor);
1052 
1053  if (inGame)
1054  {
1055  ic.CreateEditingHUD(componentEditor);
1056  componentEditor.Recalculate();
1057  continue;
1058  }
1059 
1060  List<RelatedItem> requiredItems = new List<RelatedItem>();
1061  foreach (var kvp in ic.RequiredItems)
1062  {
1063  foreach (RelatedItem relatedItem in kvp.Value)
1064  {
1065  requiredItems.Add(relatedItem);
1066  }
1067  }
1068  requiredItems.AddRange(ic.DisabledRequiredItems);
1069 
1070  foreach (RelatedItem relatedItem in requiredItems)
1071  {
1072  //TODO: add to localization
1073  var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)),
1074  relatedItem.Type.ToString() + " required", font: GUIStyle.SmallFont)
1075  {
1076  Padding = new Vector4(10.0f, 0.0f, 10.0f, 0.0f)
1077  };
1078  textBlock.RectTransform.IsFixedSize = true;
1079  componentEditor.AddCustomContent(textBlock, 1);
1080 
1081  GUITextBox namesBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight))
1082  {
1083  Font = GUIStyle.SmallFont,
1084  Text = relatedItem.JoinedIdentifiers,
1085  OverflowClip = true
1086  };
1087  textBlock.RectTransform.Resize(new Point(textBlock.Rect.Width, namesBox.RectTransform.MinSize.Y));
1088 
1089  namesBox.OnDeselected += (textBox, key) =>
1090  {
1091  relatedItem.JoinedIdentifiers = textBox.Text;
1092  textBox.Text = relatedItem.JoinedIdentifiers;
1093  };
1094 
1095  namesBox.OnEnterPressed += (textBox, text) =>
1096  {
1097  relatedItem.JoinedIdentifiers = text;
1098  textBox.Text = relatedItem.JoinedIdentifiers;
1099  return true;
1100  };
1101  }
1102 
1103  ic.CreateEditingHUD(componentEditor);
1104  componentEditor.Recalculate();
1105  }
1106 
1108  SetHUDLayout();
1109 
1110  return editingHUD;
1111  }
1112 
1113  private ImmutableArray<DecorativeSprite> GetUpgradeSprites(Upgrade upgrade)
1114  {
1115  var upgradeSprites = upgrade.Prefab.DecorativeSprites;
1116 
1117  if (Prefab.UpgradeOverrideSprites.ContainsKey(upgrade.Prefab.Identifier))
1118  {
1119  upgradeSprites = Prefab.UpgradeOverrideSprites[upgrade.Prefab.Identifier];
1120  }
1121 
1122  return upgradeSprites;
1123  }
1124 
1125  public override bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false)
1126  {
1127  if (upgrade.Prefab.IsWallUpgrade) { return false; }
1128  bool result = base.AddUpgrade(upgrade, createNetworkEvent);
1129  if (result && !upgrade.Disposed)
1130  {
1131  var upgradeSprites = GetUpgradeSprites(upgrade);
1132 
1133  if (upgradeSprites.Any())
1134  {
1135  foreach (DecorativeSprite decorativeSprite in upgradeSprites)
1136  {
1137  decorativeSprite.Sprite.EnsureLazyLoaded();
1138  spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State());
1139  }
1140  UpdateSpriteStates(0.0f);
1141  }
1142  }
1143  return result;
1144  }
1145 
1146  public void CreateContainerTagPicker([MaybeNull] GUITextBox tagTextBox)
1147  {
1148  var msgBox = new GUIMessageBox(string.Empty, string.Empty, new[] { TextManager.Get("Ok") }, new Vector2(0.35f, 0.6f), new Point(400, 400));
1149  msgBox.Buttons[0].OnClicked = msgBox.Close;
1150 
1151  var infoIcon = new GUIImage(new RectTransform(new Vector2(0.066f), msgBox.InnerFrame.RectTransform)
1152  {
1153  RelativeOffset = new Vector2(0.015f)
1154  }, style: "GUIButtonInfo")
1155  {
1156  ToolTip = TextManager.Get("containertagui.tutorial"),
1157  IgnoreLayoutGroups = true
1158  };
1159 
1160  var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.85f), msgBox.Content.RectTransform));
1161 
1162  var list = new GUIListBox(new RectTransform(new Vector2(1f, 1f), layout.RectTransform));
1163 
1164  const float NameSize = 0.4f;
1165  const float ItemSize = 0.5f;
1166  const float CountSize = 0.1f;
1167 
1168  var headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), list.Content.RectTransform), isHorizontal: true);
1169  new GUIButton(new RectTransform(new Vector2(NameSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.tag"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false };
1170  new GUIButton(new RectTransform(new Vector2(ItemSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.items"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false };
1171  new GUIButton(new RectTransform(new Vector2(CountSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.count"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false };
1172 
1173  var itemsByTag =
1174  ContainerTagPrefab.Prefabs
1175  .ToImmutableDictionary(
1176  ct => ct,
1177  ct => ct.GetItemsAndSpawnProbabilities());
1178 
1179  // Group the prefabs by category and turn them into a dictionary where the key is the category and value is the list of identifiers of the prefabs.
1180  // LINQ GroupBy returns GroupedEnumerable where the enumerable is the list of prefabs and key is what we grouped by.
1181  var tagCategories = ContainerTagPrefab.Prefabs
1182  .GroupBy(ct => ct.Category)
1183  .ToImmutableDictionary(
1184  g => g.Key,
1185  g => g.Select(ct => ct.Identifier).ToImmutableArray());
1186 
1187  foreach (var (category, categoryTags) in tagCategories)
1188  {
1189  var categoryButton = new GUIButton(new RectTransform(new Vector2(1f, 0.075f), list.Content.RectTransform), style: "GUIButtonSmallFreeScale");
1190  categoryButton.Color *= 0.66f;
1191  var categoryLayout = new GUILayoutGroup(new RectTransform(Vector2.One, categoryButton.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
1192  var categoryText = new GUITextBlock(new RectTransform(Vector2.One, categoryLayout.RectTransform), TextManager.Get($"tagcategory.{category}"), font: GUIStyle.SubHeadingFont);
1193  var arrowImage = new GUIImage(new RectTransform(new Vector2(1f, 0.5f), categoryLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrowFreeScale");
1194  var arrowPadding = new GUIFrame(new RectTransform(new Vector2(0.025f, 1f), categoryLayout.RectTransform), style: null);
1195 
1196  bool hasHiddenCategories = false;
1197  foreach (var categoryTag in categoryTags.OrderBy(t => t.Value))
1198  {
1199  var found = itemsByTag.FirstOrNull(kvp => kvp.Key.Identifier == categoryTag);
1200  if (found is null)
1201  {
1202  DebugConsole.ThrowError($"Failed to find tag with identifier {categoryTag} in itemsByTag");
1203  continue;
1204  }
1205 
1206  var (tag, prefabsAndProbabilities) = found.Value;
1207 
1208  bool isCorrectSubType = tag.IsRecommendedForSub(Submarine);
1209 
1210  var tagLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), list.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
1211  {
1212  UserData = category,
1213  Visible = isCorrectSubType
1214  };
1215 
1216  if (!isCorrectSubType)
1217  {
1218  hasHiddenCategories = true;
1219  }
1220 
1221  var checkBoxLayout = new GUILayoutGroup(new RectTransform(new Vector2(NameSize, 1f), tagLayout.RectTransform), childAnchor: Anchor.Center);
1222  var enabledCheckBox = new GUITickBox(new RectTransform(Vector2.One, checkBoxLayout.RectTransform, Anchor.Center), tag.Name, font: GUIStyle.SmallFont)
1223  {
1224  Selected = tags.Contains(tag.Identifier),
1225  ToolTip = tag.Description
1226  };
1227 
1228  var tickBoxText = enabledCheckBox.TextBlock;
1229  tickBoxText.Text = ToolBox.LimitString(tickBoxText.Text, tickBoxText.Font, tickBoxText.Rect.Width);
1230 
1231  var itemLayout = new GUILayoutGroup(new RectTransform(new Vector2(ItemSize, 1f), tagLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
1232  var itemLayoutScissor = new GUIScissorComponent(new RectTransform(new Vector2(0.8f, 1f), itemLayout.RectTransform)) { CanBeFocused = false };
1233  var itemLayoutButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1), itemLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center);
1234  var itemLayoutButton = new GUIButton(new RectTransform(new Vector2(0.8f), itemLayoutButtonLayout.RectTransform), text: "...", style: "GUICharacterInfoButton")
1235  {
1236  UserData = tag,
1237  ToolTip = TextManager.Get("containertagui.viewprobabilities")
1238  };
1239 
1240  itemLayoutButtonLayout.Recalculate();
1241 
1242  float scroll = 0f;
1243  float localScroll = 0f;
1244  int lastSkippedItems = 0;
1245  int skippedItems = 0;
1246  var itemLayoutDraw = new GUICustomComponent(new RectTransform(new Vector2(1f, 0.9f), itemLayoutScissor.Content.RectTransform, Anchor.CenterLeft), onDraw: (spriteBatch, component) =>
1247  {
1248  component.ToolTip = string.Empty;
1249 
1250  const float padding = 8f;
1251  float offset = 0f;
1252  float size = component.Rect.Height;
1253  int start = (int)Math.Floor(scroll);
1254  int amountToDraw = (int)Math.Ceiling(component.Rect.Width / size) + 1; // +1 just to be on the safe side
1255  bool shouldIncrementOnSkip = true;
1256  float toDrawWidth = prefabsAndProbabilities.Length * (size + padding);
1257 
1258  // if the width is less than the component width we need to limit how many items we draw or it looks weird
1259  if (toDrawWidth < component.Rect.Width)
1260  {
1261  shouldIncrementOnSkip = false;
1262  amountToDraw = prefabsAndProbabilities.Length;
1263  }
1264 
1265  for (int i = start; i < start + amountToDraw; i++)
1266  {
1267  var (ip, probability, _) = prefabsAndProbabilities[i % prefabsAndProbabilities.Length];
1268  var sprite = ip.InventoryIcon ?? ip.Sprite;
1269 
1270  if (sprite is null)
1271  {
1272  // I don't think this should happen but just in case
1273  if (shouldIncrementOnSkip)
1274  {
1275  amountToDraw++;
1276  skippedItems++;
1277  }
1278  continue;
1279  }
1280 
1281  if (ShouldHideItemPrefab(ip, probability))
1282  {
1283  if (shouldIncrementOnSkip)
1284  {
1285  skippedItems++;
1286  amountToDraw++;
1287  }
1288  continue;
1289  }
1290 
1291  float partialScroll = localScroll * (size + padding);
1292  var drawRect = new RectangleF(itemLayoutScissor.Rect.X + offset - partialScroll, component.Rect.Y, size, size);
1293 
1294  var isMouseOver = drawRect.Contains(PlayerInput.MousePosition);
1295  if (isMouseOver)
1296  {
1297  component.ToolTip = ip.CreateTooltipText();
1298  }
1299 
1300  var slotSprite = Inventory.SlotSpriteSmall;
1301  slotSprite?.Draw(spriteBatch, drawRect.Location, Color.White, origin: Vector2.Zero, rotate: 0f, scale: size / slotSprite.size.X * Inventory.SlotSpriteSmallScale);
1302 
1303  float iconScale = Math.Min(drawRect.Width / sprite.size.X, drawRect.Height / sprite.size.Y) * 0.9f;
1304 
1305  Color drawColor = ip.InventoryIconColor;
1306 
1307  sprite.Draw(spriteBatch, drawRect.Center, drawColor, origin: sprite.Origin, scale: iconScale);
1308  offset += size + padding;
1309  }
1310 
1311  // we need to compensate for the skipped items so that the scroll doesn't jump around
1312  if (skippedItems < lastSkippedItems)
1313  {
1314  scroll += lastSkippedItems - skippedItems;
1315  }
1316 
1317  lastSkippedItems = skippedItems;
1318  skippedItems = 0;
1319  }, onUpdate: (deltaTime, component) =>
1320  {
1321  if (GUI.MouseOn != component && MathUtils.NearlyEqual(localScroll, 0, deltaTime * 2))
1322  {
1323  localScroll = 0f;
1324  return;
1325  }
1326 
1327  float totalWidth = prefabsAndProbabilities.Length * (component.Rect.Height + 8f);
1328  if (totalWidth < component.Rect.Width) { return; }
1329  scroll += deltaTime;
1330  localScroll = scroll % 1f;
1331  })
1332  {
1333  HoverCursor = CursorState.Default,
1334  AlwaysOverrideCursor = true
1335  };
1336 
1337  var tooltip = TextManager.Get(tag.WarnIfLess ? "ContainerTagUI.RecommendedAmount" : "ContainerTagUI.SuggestedAmount");
1338 
1339  var countBlock = new GUITextBlock(new RectTransform(new Vector2(CountSize, 1f), tagLayout.RectTransform), string.Empty, textAlignment: Alignment.Center)
1340  {
1341  ToolTip = tooltip
1342  };
1343  UpdateCountBlock(countBlock, tag);
1344 
1345  enabledCheckBox.OnSelected += tickBox =>
1346  {
1347  if (tickBox.Selected)
1348  {
1349  AddTag(tag.Identifier);
1350  }
1351  else
1352  {
1353  RemoveTag(tag.Identifier);
1354  }
1355 
1356  if (tagTextBox is not null)
1357  {
1358  tagTextBox.Text = string.Join(',', tags.Where(t => !Prefab.Tags.Contains(t)));
1359  }
1360  UpdateCountBlock(countBlock, tag);
1361  return true;
1362  };
1363 
1364  itemLayoutButton.OnClicked = (button, _) =>
1365  {
1366  CreateContainerTagItemListPopup(tag, button.Rect.Center, layout, prefabsAndProbabilities);
1367  return true;
1368  };
1369 
1370  void UpdateCountBlock(GUITextBlock textBlock, ContainerTagPrefab containerTag)
1371  {
1372  if (textBlock is null) { return; }
1373 
1374  var tagCount = Submarine.GetItems(alsoFromConnectedSubs: true).Count(i => i.HasTag(containerTag.Identifier));
1375  textBlock.Text = $"{tagCount} ({containerTag.RecommendedAmount})";
1376 
1377  if (!isCorrectSubType || !containerTag.WarnIfLess || containerTag.RecommendedAmount <= 0) { return; }
1378 
1379  if (tagCount < containerTag.RecommendedAmount)
1380  {
1381  textBlock.TextColor = GUIStyle.Red;
1382  textBlock.Text += "*";
1383  textBlock.ToolTip = RichString.Rich($"{tooltip}\n\n‖color:gui.red‖{TextManager.Get("ContainerTagUI.RecommendedAmountWarning")}‖color:end‖");
1384  }
1385  else if (tagCount >= containerTag.RecommendedAmount)
1386  {
1387  textBlock.TextColor = GUIStyle.Green;
1388  textBlock.ToolTip = tooltip;
1389  }
1390  }
1391  }
1392 
1393  arrowImage.SpriteEffects = hasHiddenCategories ? SpriteEffects.None : SpriteEffects.FlipVertically;
1394  categoryButton.OnClicked = (_, _) =>
1395  {
1396  arrowImage.SpriteEffects ^= SpriteEffects.FlipVertically;
1397 
1398  foreach (var child in list.Content.Children)
1399  {
1400  if (child.UserData is Identifier id && id == category)
1401  {
1402  child.Visible = !child.Visible;
1403  }
1404  }
1405  return true;
1406  };
1407  }
1408  }
1409 
1410  private static void CreateContainerTagItemListPopup(ContainerTagPrefab tag, Point location, GUIComponent popupParent, ImmutableArray<ContainerTagPrefab.ItemAndProbability> prefabAndProbabilities)
1411  {
1412  const string TooltipUserData = "tooltip";
1413  const string ProbabilityUserData = "probability";
1414 
1415  if (popupParent.GetChildByUserData(TooltipUserData) is { } existingTooltip)
1416  {
1417  popupParent.RemoveChild(existingTooltip);
1418  }
1419 
1420  var tooltip = new GUIFrame(new RectTransform(new Point(popupParent.Rect.Height), popupParent.RectTransform)
1421  {
1422  AbsoluteOffset = location - popupParent.Rect.Location
1423  })
1424  {
1425  UserData = TooltipUserData,
1426  IgnoreLayoutGroups = true
1427  };
1428 
1429  if (tooltip.Rect.Bottom > GameMain.GraphicsHeight)
1430  {
1431  int diffY = tooltip.Rect.Bottom - GameMain.GraphicsHeight;
1432  tooltip.RectTransform.AbsoluteOffset -= new Point(0, diffY);
1433  }
1434 
1435  if (tooltip.Rect.Right > GameMain.GraphicsWidth)
1436  {
1437  int diffX = tooltip.Rect.Right - GameMain.GraphicsWidth;
1438  tooltip.RectTransform.AbsoluteOffset -= new Point(diffX, 0);
1439  }
1440 
1441  var tooltipLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(tooltip.RectTransform, 0.9f), tooltip.RectTransform, Anchor.Center));
1442 
1443  var tooltipHeader = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), tag.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont);
1444  var tooltipList = new GUIListBox(new RectTransform(new Vector2(1f, 0.7f), tooltipLayout.RectTransform));
1445 
1446  var tooltipHeaderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), tooltipList.Content.RectTransform), isHorizontal: true);
1447  new GUIButton(new RectTransform(new Vector2(0.66f, 1f), tooltipHeaderLayout.RectTransform), TextManager.Get("tagheader.item"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false };
1448  new GUIButton(new RectTransform(new Vector2(0.33f, 1f), tooltipHeaderLayout.RectTransform), TextManager.Get("tagheader.probability"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false };
1449 
1450  foreach (var itemAndProbability in prefabAndProbabilities.OrderByDescending(p => p.Probability))
1451  {
1452  var (ip, probability, campaignOnlyProbability) = itemAndProbability;
1453  if (ShouldHideItemPrefab(ip, probability)) { continue; }
1454 
1455  var itemLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), tooltipList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
1456  {
1457  UserData = itemAndProbability
1458  };
1459 
1460  var itemNameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.66f, 1f), itemLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true)
1461  {
1462  Stretch = true
1463  };
1464 
1465  var itemIcon = new GUIImage(new RectTransform(Vector2.One, itemNameLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), ip.InventoryIcon ?? ip.Sprite, scaleToFit: true)
1466  {
1467  Color = ip.InventoryIconColor
1468  };
1469 
1470  var itemName = new GUITextBlock(new RectTransform(Vector2.One, itemNameLayout.RectTransform), ip.Name);
1471  itemName.Text = ToolBox.LimitString(ip.Name, itemName.Font, itemName.Rect.Width);
1472 
1473  var toolTipContainer = new GUIFrame(new RectTransform(Vector2.One, itemNameLayout.RectTransform), style: null)
1474  {
1475  IgnoreLayoutGroups = true,
1476  ToolTip = ip.CreateTooltipText()
1477  };
1478 
1479  var probabilityText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1f), itemLayout.RectTransform), ProbabilityToPercentage(campaignOnlyProbability), textAlignment: Alignment.Right)
1480  {
1481  UserData = ProbabilityUserData
1482  };
1483  if (MathUtils.NearlyEqual(campaignOnlyProbability, 0f)) { probabilityText.TextColor = GUIStyle.Red; }
1484  }
1485 
1486  var campaignCheckbox = new GUITickBox(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), label: TextManager.Get("containertagui.campaignonly"))
1487  {
1488  ToolTip = TextManager.Get("containertagui.campaignonlytooltip"),
1489  Selected = true,
1490  OnSelected = box =>
1491  {
1492  foreach (var child in tooltipList.Content.Children)
1493  {
1494  if (child.UserData is not ContainerTagPrefab.ItemAndProbability data) { continue; }
1495 
1496  if (child.GetChildByUserData(ProbabilityUserData) is not GUITextBlock text) { continue; }
1497 
1498  float probability = box.Selected
1499  ? data.CampaignProbability
1500  : data.Probability;
1501  text.Text = ProbabilityToPercentage(probability);
1502 
1503  text.TextColor = MathUtils.NearlyEqual(probability, 0f)
1504  ? GUIStyle.Red
1505  : GUIStyle.TextColorNormal;
1506  }
1507 
1508  return true;
1509  }
1510  };
1511 
1512  var tooltipClose = new GUIButton(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), TextManager.Get("Close"))
1513  {
1514  OnClicked = (_, _) =>
1515  {
1516  popupParent.RemoveChild(tooltip);
1517  return true;
1518  }
1519  };
1520 
1521  static LocalizedString ProbabilityToPercentage(float probability)
1522  => TextManager.GetWithVariable("percentageformat", "[value]", MathF.Round((probability * 100f), 1).ToString(CultureInfo.InvariantCulture));
1523 
1524  }
1525 
1526  private static bool ShouldHideItemPrefab(ItemPrefab ip, float probability)
1527  => ip.HideInMenus && MathUtils.NearlyEqual(probability, 0f);
1528 
1532  private void SetHUDLayout(bool ignoreLocking = false)
1533  {
1534  //reset positions first
1535  List<GUIComponent> elementsToMove = new List<GUIComponent>();
1536 
1537  if (editingHUD != null && editingHUD.UserData == this &&
1538  ((HasInGameEditableProperties && Character.Controlled?.SelectedItem == this) || Screen.Selected == GameMain.SubEditorScreen))
1539  {
1540  elementsToMove.Add(editingHUD);
1541  }
1542 
1543  debugInitialHudPositions.Clear();
1544  foreach (ItemComponent ic in activeHUDs)
1545  {
1546  if (ic.GuiFrame == null || ic.AllowUIOverlap || ic.GetLinkUIToComponent() != null) { continue; }
1547  if (!ignoreLocking && ic.LockGuiFramePosition) { continue; }
1548  //if the frame covers nearly all of the screen, don't trying to prevent overlaps because it'd fail anyway
1549  if (ic.GuiFrame.Rect.Width >= GameMain.GraphicsWidth * 0.9f && ic.GuiFrame.Rect.Height >= GameMain.GraphicsHeight * 0.9f) { continue; }
1551  elementsToMove.Add(ic.GuiFrame);
1552  debugInitialHudPositions.Add(ic.GuiFrame.Rect);
1553  }
1554 
1555  List<Rectangle> disallowedAreas = new List<Rectangle>();
1556  if (GameMain.GameSession?.CrewManager != null && Screen.Selected == GameMain.GameScreen)
1557  {
1558  int disallowedPadding = (int)(50 * GUI.Scale);
1559  disallowedAreas.Add(GameMain.GameSession.CrewManager.GetActiveCrewArea());
1560  disallowedAreas.Add(new Rectangle(
1561  HUDLayoutSettings.ChatBoxArea.X - disallowedPadding, HUDLayoutSettings.ChatBoxArea.Y,
1562  HUDLayoutSettings.ChatBoxArea.Width + disallowedPadding, HUDLayoutSettings.ChatBoxArea.Height));
1563  }
1564 
1565  if (Screen.Selected is SubEditorScreen editor)
1566  {
1567  disallowedAreas.Add(editor.EntityMenu.Rect);
1568  disallowedAreas.Add(editor.TopPanel.Rect);
1569  disallowedAreas.Add(editor.ToggleEntityMenuButton.Rect);
1570  }
1571 
1572  GUI.PreventElementOverlap(elementsToMove, disallowedAreas, clampArea: HUDLayoutSettings.ItemHUDArea);
1573 
1574  //System.Diagnostics.Debug.WriteLine("after: " + elementsToMove[0].Rect.ToString() + " " + elementsToMove[1].Rect.ToString());
1575  foreach (ItemComponent ic in activeHUDs)
1576  {
1577  if (ic.GuiFrame == null) { continue; }
1578 
1579  var linkUIToComponent = ic.GetLinkUIToComponent();
1580  if (linkUIToComponent == null) { continue; }
1581 
1582  ic.GuiFrame.RectTransform.ScreenSpaceOffset = linkUIToComponent.GuiFrame.RectTransform.ScreenSpaceOffset;
1583  }
1584  }
1585 
1586  private readonly List<Rectangle> debugInitialHudPositions = new List<Rectangle>();
1587 
1588  private readonly List<ItemComponent> prevActiveHUDs = new List<ItemComponent>();
1589  private readonly List<ItemComponent> activeComponents = new List<ItemComponent>();
1590  private readonly List<ItemComponent> maxPriorityHUDs = new List<ItemComponent>();
1591 
1592  public void UpdateHUD(Camera cam, Character character, float deltaTime)
1593  {
1594  bool editingHUDCreated = false;
1595  if ((HasInGameEditableProperties && (character.SelectedItem == this || EditableWhenEquipped)) ||
1597  {
1598  GUIComponent prevEditingHUD = editingHUD;
1599  UpdateEditing(cam, deltaTime);
1600  editingHUDCreated = editingHUD != null && editingHUD != prevEditingHUD;
1601  }
1602 
1603  if (editingHUD == null ||
1604  !(GUI.KeyboardDispatcher.Subscriber is GUITextBox textBox) ||
1605  !editingHUD.IsParentOf(textBox))
1606  {
1607  editingHUDRefreshTimer -= deltaTime;
1608  }
1609 
1610  prevActiveHUDs.Clear();
1611  prevActiveHUDs.AddRange(activeHUDs);
1612  activeComponents.Clear();
1613  activeComponents.AddRange(components);
1614 
1615  foreach (MapEntity entity in linkedTo)
1616  {
1617  if (Prefab.IsLinkAllowed(entity.Prefab) && entity is Item i)
1618  {
1619  if (!i.DisplaySideBySideWhenLinked) { continue; }
1620  activeComponents.AddRange(i.components);
1621  }
1622  }
1623 
1624  activeHUDs.Clear();
1625  maxPriorityHUDs.Clear();
1626  bool DrawHud(ItemComponent ic)
1627  {
1628  if (!ic.ShouldDrawHUD(character)) { return false; }
1629  if (character.HasEquippedItem(this))
1630  {
1631  return ic.DrawHudWhenEquipped;
1632  }
1633  else
1634  {
1635  return ic.CanBeSelected && ic.HasRequiredItems(character, addMessage: false);
1636  }
1637  }
1638  //the HUD of the component with the highest priority will be drawn
1639  //if all components have a priority of 0, all of them are drawn
1640  foreach (ItemComponent ic in activeComponents)
1641  {
1642  if (ic.HudPriority > 0 && DrawHud(ic) && (maxPriorityHUDs.Count == 0 || ic.HudPriority >= maxPriorityHUDs[0].HudPriority))
1643  {
1644  if (maxPriorityHUDs.Count > 0 && ic.HudPriority > maxPriorityHUDs[0].HudPriority) { maxPriorityHUDs.Clear(); }
1645  maxPriorityHUDs.Add(ic);
1646  }
1647  }
1648 
1649  if (maxPriorityHUDs.Count > 0)
1650  {
1651  activeHUDs.AddRange(maxPriorityHUDs);
1652  }
1653  else
1654  {
1655  foreach (ItemComponent ic in activeComponents)
1656  {
1657  if (DrawHud(ic))
1658  {
1659  activeHUDs.Add(ic);
1660  }
1661  }
1662  }
1663 
1664  activeHUDs.Sort((h1, h2) => { return h2.HudLayer.CompareTo(h1.HudLayer); });
1665 
1666  //active HUDs have changed, need to reposition
1667  if (!prevActiveHUDs.SequenceEqual(activeHUDs) || editingHUDCreated)
1668  {
1669  SetHUDLayout();
1670  }
1671 
1672  Rectangle mergedHUDRect = Rectangle.Empty;
1673  foreach (ItemComponent ic in activeHUDs)
1674  {
1675  ic.UpdateHUD(character, deltaTime, cam);
1676  if (ic.GuiFrame != null && ic.GuiFrame.Rect.Height < GameMain.GraphicsHeight)
1677  {
1678  mergedHUDRect = mergedHUDRect == Rectangle.Empty ?
1679  ic.GuiFrame.Rect :
1680  Rectangle.Union(mergedHUDRect, ic.GuiFrame.Rect);
1681  }
1682  }
1683 
1684  if (mergedHUDRect != Rectangle.Empty)
1685  {
1686  if (itemInUseWarning != null) { itemInUseWarning.Visible = false; }
1687  foreach (Character otherCharacter in Character.CharacterList)
1688  {
1689  if (otherCharacter != character &&
1690  otherCharacter.SelectedItem == this)
1691  {
1692  ItemInUseWarning.Visible = true;
1693  if (mergedHUDRect.Width > GameMain.GraphicsWidth / 2) { mergedHUDRect.Inflate(-GameMain.GraphicsWidth / 4, 0); }
1694  itemInUseWarning.RectTransform.ScreenSpaceOffset = new Point(mergedHUDRect.X, mergedHUDRect.Bottom);
1695  itemInUseWarning.RectTransform.NonScaledSize = new Point(mergedHUDRect.Width, (int)(50 * GUI.Scale));
1696  if (itemInUseWarning.UserData != otherCharacter)
1697  {
1698  itemInUseWarning.Text = TextManager.GetWithVariable("ItemInUse", "[character]", otherCharacter.Name);
1699  itemInUseWarning.UserData = otherCharacter;
1700  }
1701  break;
1702  }
1703  }
1704  }
1705  }
1706 
1707  public void DrawHUD(SpriteBatch spriteBatch, Camera cam, Character character)
1708  {
1709  if (HasInGameEditableProperties && (character.SelectedItem == this || EditableWhenEquipped))
1710  {
1711  DrawEditing(spriteBatch, cam);
1712  }
1713 
1714  foreach (ItemComponent ic in activeHUDs)
1715  {
1716  if (ic.CanBeSelected)
1717  {
1718  ic.DrawHUD(spriteBatch, character);
1719  }
1720  }
1721 
1722  if (GameMain.DebugDraw)
1723  {
1724  int i = 0;
1725  foreach (ItemComponent ic in activeHUDs)
1726  {
1727  if (i >= debugInitialHudPositions.Count) { break; }
1728  if (activeHUDs[i].GuiFrame == null) { continue; }
1729  if (ic.GuiFrame == null || ic.AllowUIOverlap || ic.GetLinkUIToComponent() != null) { continue; }
1730 
1731  GUI.DrawRectangle(spriteBatch, debugInitialHudPositions[i], Color.Orange);
1732  GUI.DrawRectangle(spriteBatch, ic.GuiFrame.Rect, Color.LightGreen);
1733  GUI.DrawLine(spriteBatch, debugInitialHudPositions[i].Location.ToVector2(), ic.GuiFrame.Rect.Location.ToVector2(), Color.Orange);
1734 
1735  i++;
1736  }
1737  }
1738  }
1739 
1740  readonly List<ColoredText> texts = new();
1741  public List<ColoredText> GetHUDTexts(Character character, bool recreateHudTexts = true)
1742  {
1743  // Always create the texts if they have not yet been created
1744  if (texts.Any() && !recreateHudTexts) { return texts; }
1745  texts.Clear();
1746 
1747  string nameText = Name;
1748  if (Prefab.Tags.Contains("identitycard") || Tags.Contains("despawncontainer"))
1749  {
1750  string[] readTags = Tags.Split(',');
1751  string idName = null;
1752  foreach (string tag in readTags)
1753  {
1754  string[] s = tag.Split(':');
1755  if (s[0] == "name")
1756  {
1757  idName = s[1];
1758  break;
1759  }
1760  }
1761  if (idName != null)
1762  {
1763  nameText += $" ({idName})";
1764  }
1765  }
1766  if (DroppedStack.Any())
1767  {
1768  nameText += $" x{DroppedStack.Count()}";
1769  }
1770 
1771  texts.Add(new ColoredText(nameText, GUIStyle.TextColorNormal, isCommand: false, isError: false));
1772 
1773  if (CampaignMode.BlocksInteraction(CampaignInteractionType))
1774  {
1775  texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)).Value, Color.Cyan, isCommand: false, isError: false));
1776  }
1777  else
1778  {
1779  foreach (ItemComponent itemComponent in components)
1780  {
1781  var interactionVisibility = GetComponentInteractionVisibility(character, itemComponent);
1782  if (interactionVisibility == InteractionVisibility.None) { continue; }
1783  if (itemComponent.DisplayMsg.IsNullOrEmpty()) { continue; }
1784 
1785  Color color = interactionVisibility == InteractionVisibility.MissingRequirement ? Color.Gray : Color.Cyan;
1786  texts.Add(new ColoredText(itemComponent.DisplayMsg.Value, color, isCommand: false, isError: false));
1787  }
1788  }
1789  if (PlayerInput.KeyDown(InputType.ContextualCommand))
1790  {
1791  texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")).Value, Color.Cyan, isCommand: false, isError: false));
1792  }
1793  else
1794  {
1795  texts.Add(new ColoredText(TextManager.Get("itemmsg.morreoptionsavailable").Value, Color.LightGray * 0.7f, isCommand: false, isError: false));
1796  }
1797  return texts;
1798  }
1799 
1800  private enum InteractionVisibility
1801  {
1802  None,
1803  MissingRequirement,
1804  Visible
1805  }
1806 
1818  private static InteractionVisibility GetComponentInteractionVisibility(Character character, ItemComponent itemComponent)
1819  {
1820  if (!itemComponent.CanBePicked && !itemComponent.CanBeSelected) { return InteractionVisibility.None; }
1821  if (itemComponent is Holdable holdable && !holdable.CanBeDeattached()) { return InteractionVisibility.None; }
1822  if (itemComponent is ConnectionPanel connectionPanel && !connectionPanel.CanRewire()) { return InteractionVisibility.None; }
1823 
1824  InteractionVisibility interactionVisibility = InteractionVisibility.MissingRequirement;
1825  if (itemComponent.HasRequiredItems(character, addMessage: false))
1826  {
1827  if (itemComponent is Repairable repairable)
1828  {
1829  if (repairable.IsBelowRepairThreshold)
1830  {
1831  interactionVisibility = InteractionVisibility.Visible;
1832  }
1833  }
1834  else
1835  {
1836  interactionVisibility = InteractionVisibility.Visible;
1837  }
1838  }
1839 
1840  return interactionVisibility;
1841  }
1842 
1843  public bool HasVisibleInteraction(Character character)
1844  {
1845  foreach (var component in components)
1846  {
1847  if (GetComponentInteractionVisibility(character, component) == InteractionVisibility.Visible)
1848  {
1849  return true;
1850  }
1851  }
1852 
1853  return false;
1854  }
1855 
1856  public void ForceHUDLayoutUpdate(bool ignoreLocking = false)
1857  {
1858  foreach (ItemComponent ic in activeHUDs)
1859  {
1860  if (ic.GuiFrame == null) { continue; }
1861  if (!ic.CanBeSelected && !ic.DrawHudWhenEquipped) { continue; }
1862  ic.GuiFrame.RectTransform.ScreenSpaceOffset = Point.Zero;
1863  if (ic.UseAlternativeLayout)
1864  {
1866  }
1867  else
1868  {
1870  }
1871  }
1872  SetHUDLayout(ignoreLocking);
1873  }
1874 
1875  public override void AddToGUIUpdateList(int order = 0)
1876  {
1878  {
1879  if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(); }
1880  }
1881  else
1882  {
1883  if (HasInGameEditableProperties && Character.Controlled != null && (Character.Controlled.SelectedItem == this || EditableWhenEquipped))
1884  {
1885  if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(); }
1886  }
1887  }
1888 
1889  var character = Character.Controlled;
1890  var selectedItem = Character.Controlled?.SelectedItem;
1891  if (character != null && selectedItem != this && GetComponent<RemoteController>() == null)
1892  {
1893  bool insideCircuitBox =
1894  selectedItem?.GetComponent<CircuitBox>() != null &&
1895  selectedItem.ContainedItems.Contains(this);
1896  if (!insideCircuitBox &&
1897  selectedItem?.GetComponent<RemoteController>()?.TargetItem != this &&
1898  !character.HeldItems.Any(it => it.GetComponent<RemoteController>()?.TargetItem == this))
1899  {
1900  return;
1901  }
1902  }
1903 
1904  bool needsLayoutUpdate = false;
1905  foreach (ItemComponent ic in activeHUDs)
1906  {
1907  if (!ic.CanBeSelected) { continue; }
1908 
1909  bool useAlternativeLayout = activeHUDs.Count > 1;
1910  bool wasUsingAlternativeLayout = ic.UseAlternativeLayout;
1911  ic.UseAlternativeLayout = useAlternativeLayout;
1912  needsLayoutUpdate |= ic.UseAlternativeLayout != wasUsingAlternativeLayout;
1913  ic.AddToGUIUpdateList(order);
1914  }
1915 
1916  if (itemInUseWarning != null && itemInUseWarning.Visible)
1917  {
1918  itemInUseWarning.AddToGUIUpdateList();
1919  }
1920 
1921  if (needsLayoutUpdate)
1922  {
1923  SetHUDLayout();
1924  }
1925  }
1926 
1927  public void ClientEventRead(IReadMessage msg, float sendingTime)
1928  {
1929  EventType eventType =
1930  (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue);
1931 
1932  switch (eventType)
1933  {
1934  case EventType.ComponentState:
1935  {
1936  int componentIndex = msg.ReadRangedInteger(0, components.Count - 1);
1937  if (components[componentIndex] is IServerSerializable serverSerializable)
1938  {
1939  serverSerializable.ClientEventRead(msg, sendingTime);
1940  }
1941  else
1942  {
1943  throw new Exception($"Failed to read component state - {components[componentIndex].GetType()} in item \"{Prefab.Identifier}\" is not IServerSerializable.");
1944  }
1945  }
1946  break;
1947  case EventType.InventoryState:
1948  {
1949  int containerIndex = msg.ReadRangedInteger(0, components.Count - 1);
1950  if (components[containerIndex] is ItemContainer container)
1951  {
1952  container.Inventory.ClientEventRead(msg);
1953  }
1954  else
1955  {
1956  throw new Exception($"Failed to read inventory state - {components[containerIndex].GetType()} in item \"{Prefab.Identifier}\" is not an ItemContainer.");
1957  }
1958  }
1959  break;
1960  case EventType.Status:
1961  bool loadingRound = msg.ReadBoolean();
1962  float newCondition = msg.ReadSingle();
1963  SetCondition(newCondition, isNetworkEvent: true, executeEffects: !loadingRound);
1964  break;
1965  case EventType.AssignCampaignInteraction:
1966  bool isVisible = msg.ReadBoolean();
1967  if (isVisible)
1968  {
1969  var interactionType = (CampaignMode.InteractionType)msg.ReadByte();
1970  AssignCampaignInteractionType(interactionType);
1971  }
1972  break;
1973  case EventType.ApplyStatusEffect:
1974  {
1975  ActionType actionType = (ActionType)msg.ReadRangedInteger(0, Enum.GetValues(typeof(ActionType)).Length - 1);
1976  byte componentIndex = msg.ReadByte();
1977  ushort targetCharacterID = msg.ReadUInt16();
1978  byte targetLimbID = msg.ReadByte();
1979  ushort useTargetID = msg.ReadUInt16();
1980  Vector2? worldPosition = null;
1981  bool hasPosition = msg.ReadBoolean();
1982  if (hasPosition)
1983  {
1984  worldPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle());
1985  }
1986 
1987  ItemComponent targetComponent = componentIndex < components.Count ? components[componentIndex] : null;
1988  Character targetCharacter = FindEntityByID(targetCharacterID) as Character;
1989  Limb targetLimb = targetCharacter != null && targetLimbID < targetCharacter.AnimController.Limbs.Length ?
1990  targetCharacter.AnimController.Limbs[targetLimbID] : null;
1991  Entity useTarget = FindEntityByID(useTargetID);
1992 
1993  if (targetComponent == null)
1994  {
1995  ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, isNetworkEvent: true, worldPosition: worldPosition);
1996  }
1997  else
1998  {
1999  targetComponent.ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, worldPosition: worldPosition);
2000  }
2001  }
2002  break;
2003  case EventType.ChangeProperty:
2004  ReadPropertyChange(msg, false);
2005  break;
2006  case EventType.ItemStat:
2007  byte length = msg.ReadByte();
2008  for (int i = 0; i < length; i++)
2009  {
2010  var statIdentifier = INetSerializableStruct.Read<TalentStatIdentifier>(msg);
2011  var statValue = msg.ReadSingle();
2012  StatManager.ApplyStatDirect(statIdentifier, statValue);
2013  }
2014  break;
2015  case EventType.Upgrade:
2016  Identifier identifier = msg.ReadIdentifier();
2017  byte level = msg.ReadByte();
2018  if (UpgradePrefab.Find(identifier) is { } upgradePrefab)
2019  {
2020  Upgrade upgrade = new Upgrade(this, upgradePrefab, level);
2021 
2022  byte targetCount = msg.ReadByte();
2023  for (int i = 0; i < targetCount; i++)
2024  {
2025  byte propertyCount = msg.ReadByte();
2026  for (int j = 0; j < propertyCount; j++)
2027  {
2028  float value = msg.ReadSingle();
2029  upgrade.TargetComponents.ElementAt(i).Value[j].SetOriginalValue(value);
2030  }
2031  }
2032 
2033  AddUpgrade(upgrade, false);
2034  }
2035  break;
2036  case EventType.DroppedStack:
2037  int itemCount = msg.ReadRangedInteger(0, Inventory.MaxPossibleStackSize);
2038  if (itemCount > 0)
2039  {
2040  List<Item> droppedStack = new List<Item>();
2041  for (int i = 0; i < itemCount; i++)
2042  {
2043  var id = msg.ReadUInt16();
2044  if (FindEntityByID(id) is not Item droppedItem)
2045  {
2046  DebugConsole.ThrowError($"Error while reading {EventType.DroppedStack} message: could not find an item with the ID {id}.");
2047  }
2048  else
2049  {
2050  droppedStack.Add(droppedItem);
2051  }
2052  }
2053  CreateDroppedStack(droppedStack, allowClientExecute: true);
2054  }
2055  else
2056  {
2057  RemoveFromDroppedStack(allowClientExecute: true);
2058  }
2059  break;
2060  case EventType.SetHighlight:
2061  bool isTargetedForThisClient = msg.ReadBoolean();
2062  if (isTargetedForThisClient)
2063  {
2064  bool highlight = msg.ReadBoolean();
2065  ExternalHighlight = highlight;
2066  if (highlight)
2067  {
2068  Color highlightColor = msg.ReadColorR8G8B8A8();
2069  HighlightColor = highlightColor;
2070  }
2071  else
2072  {
2073  HighlightColor = null;
2074  }
2075  }
2076  break;
2077  default:
2078  throw new Exception($"Malformed incoming item event: unsupported event type {eventType}");
2079  }
2080  }
2081 
2082  public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
2083  {
2084  Exception error(string reason)
2085  {
2086  string errorMsg = $"Failed to write a network event for the item \"{Name}\" - {reason}";
2087  GameAnalyticsManager.AddErrorEventOnce($"Item.ClientWrite:{Name}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
2088  return new Exception(errorMsg);
2089  }
2090 
2091  if (extraData is null) { throw error("event data was null"); }
2092  if (!(extraData is IEventData eventData)) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); }
2093 
2094  EventType eventType = eventData.EventType;
2095  msg.WriteRangedInteger((int)eventType, (int)EventType.MinValue, (int)EventType.MaxValue);
2096  switch (eventData)
2097  {
2098  case ComponentStateEventData componentStateEventData:
2099  {
2100  var component = componentStateEventData.Component;
2101  if (component is null) { throw error("component was null"); }
2102  if (component is not IClientSerializable clientSerializable) { throw error($"component was not {nameof(IClientSerializable)}"); }
2103  int componentIndex = components.IndexOf(component);
2104  if (componentIndex < 0) { throw error("component did not belong to item"); }
2105  msg.WriteRangedInteger(componentIndex, 0, components.Count - 1);
2106  clientSerializable.ClientEventWrite(msg, extraData);
2107  }
2108  break;
2109  case InventoryStateEventData inventoryStateEventData:
2110  {
2111  var container = inventoryStateEventData.Component;
2112  if (container is null) { throw error("container was null"); }
2113  int containerIndex = components.IndexOf(container);
2114  if (containerIndex < 0) { throw error("container did not belong to item"); }
2115  msg.WriteRangedInteger(containerIndex, 0, components.Count - 1);
2116  container.Inventory.ClientEventWrite(msg, inventoryStateEventData);
2117  }
2118  break;
2119  case TreatmentEventData treatmentEventData:
2120  Character targetCharacter = treatmentEventData.TargetCharacter;
2121 
2122  msg.WriteUInt16(targetCharacter.ID);
2123  msg.WriteByte(treatmentEventData.LimbIndex);
2124  break;
2125  case ChangePropertyEventData changePropertyEventData:
2126  WritePropertyChange(msg, changePropertyEventData, inGameEditableOnly: true);
2127  editingHUDRefreshTimer = 1.0f;
2128  break;
2129  case CombineEventData combineEventData:
2130  Item combineTarget = combineEventData.CombineTarget;
2131  msg.WriteUInt16(combineTarget.ID);
2132  break;
2133  default:
2134  throw error($"Unsupported event type {eventData.GetType().Name}");
2135  }
2136  }
2137 
2138  partial void UpdateNetPosition(float deltaTime)
2139  {
2140  if (GameMain.Client == null) { return; }
2141 
2142  if (parentInventory != null || body == null || !body.Enabled || Removed || (GetComponent<Projectile>() is { IsStuckToTarget: true }))
2143  {
2144  positionBuffer.Clear();
2145  return;
2146  }
2147 
2148  isActive = true;
2149 
2150  if (positionBuffer.Count > 0)
2151  {
2152  transformDirty = true;
2153  }
2154 
2155  body.CorrectPosition(positionBuffer, out Vector2 newPosition, out Vector2 newVelocity, out float newRotation, out float newAngularVelocity);
2156  body.LinearVelocity = newVelocity;
2157  body.AngularVelocity = newAngularVelocity;
2158  float distSqr = Vector2.DistanceSquared(newPosition, body.SimPosition);
2159 
2160  if (distSqr > 0.0001f ||
2161  Math.Abs(newRotation - body.Rotation) > 0.01f)
2162  {
2163  body.TargetPosition = newPosition;
2164  body.TargetRotation = newRotation;
2165  body.MoveToTargetPosition(lerp: true);
2166  if (distSqr > 10.0f * 10.0f)
2167  {
2168  //very large change in position, we need to recheck which submarine the item is in
2169  Submarine = null;
2170  UpdateTransform();
2171  }
2172  }
2173 
2174  Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition);
2175  rect.X = (int)(displayPos.X - rect.Width / 2.0f);
2176  rect.Y = (int)(displayPos.Y + rect.Height / 2.0f);
2177  }
2178 
2179  public void ClientReadPosition(IReadMessage msg, float sendingTime)
2180  {
2181  if (body == null)
2182  {
2183  string errorMsg = "Received a position update for an item with no physics body (" + Name + ")";
2184 #if DEBUG
2185  DebugConsole.ThrowError(errorMsg);
2186 #else
2187  if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
2188 #endif
2189  GameAnalyticsManager.AddErrorEventOnce("Item.ClientReadPosition:nophysicsbody", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
2190  return;
2191  }
2192 
2193 
2194  var posInfo = body.ClientRead(msg, sendingTime, parentDebugName: Name);
2195  msg.ReadPadBits();
2196 
2197  if (GetComponent<Projectile>() is { IsStuckToTarget: true }) { return; }
2198 
2199  if (posInfo != null)
2200  {
2201  int index = 0;
2202  while (index < positionBuffer.Count && sendingTime > positionBuffer[index].Timestamp)
2203  {
2204  index++;
2205  }
2206 
2207  positionBuffer.Insert(index, posInfo);
2208  }
2209  /*body.FarseerBody.Awake = awake;
2210  if (body.FarseerBody.Awake)
2211  {
2212  if ((newVelocity - body.LinearVelocity).LengthSquared() > 8.0f * 8.0f) body.LinearVelocity = newVelocity;
2213  }
2214  else
2215  {
2216  try
2217  {
2218  body.FarseerBody.Enabled = false;
2219  }
2220  catch (Exception e)
2221  {
2222  DebugConsole.ThrowError("Exception in PhysicsBody.Enabled = false (" + body.PhysEnabled + ")", e);
2223  if (body.UserData != null) DebugConsole.NewMessage("PhysicsBody UserData: " + body.UserData.GetType().ToString(), GUIStyle.Red);
2224  if (GameMain.World.ContactManager == null) DebugConsole.NewMessage("ContactManager is null!", GUIStyle.Red);
2225  else if (GameMain.World.ContactManager.BroadPhase == null) DebugConsole.NewMessage("Broadphase is null!", GUIStyle.Red);
2226  if (body.FarseerBody.FixtureList == null) DebugConsole.NewMessage("FixtureList is null!", GUIStyle.Red);
2227  }
2228  }
2229 
2230  if ((newPosition - SimPosition).Length() > body.LinearVelocity.Length() * 2.0f)
2231  {
2232  if (body.SetTransform(newPosition, newRotation))
2233  {
2234  Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition);
2235  rect.X = (int)(displayPos.X - rect.Width / 2.0f);
2236  rect.Y = (int)(displayPos.Y + rect.Height / 2.0f);
2237  }
2238  }*/
2239  }
2240 
2241  public void CreateClientEvent<T>(T ic) where T : ItemComponent, IClientSerializable
2242  => CreateClientEvent(ic, null);
2243 
2244  public void CreateClientEvent<T>(T ic, ItemComponent.IEventData extraData) where T : ItemComponent, IClientSerializable
2245  {
2246  if (GameMain.Client == null) { return; }
2247 
2248  #warning TODO: this should throw an exception
2249  if (!components.Contains(ic)) { return; }
2250 
2251  var eventData = new ComponentStateEventData(ic, extraData);
2252  if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false"); }
2253  GameMain.Client.CreateEntityEvent(this, eventData);
2254  }
2255 
2256  public static Item ReadSpawnData(IReadMessage msg, bool spawn = true)
2257  {
2258  string itemName = msg.ReadString();
2259  string itemIdentifier = msg.ReadString();
2260  bool descriptionChanged = msg.ReadBoolean();
2261  string itemDesc = "";
2262  if (descriptionChanged)
2263  {
2264  itemDesc = msg.ReadString();
2265  }
2266  ushort itemId = msg.ReadUInt16();
2267  ushort inventoryId = msg.ReadUInt16();
2268 
2269  DebugConsole.Log($"Received entity spawn message for item \"{itemName}\" (identifier: {itemIdentifier}, id: {itemId})");
2270 
2271  ItemPrefab itemPrefab =
2272  string.IsNullOrEmpty(itemIdentifier) ?
2273  ItemPrefab.Find(itemName, Identifier.Empty) :
2274  ItemPrefab.Find(itemName, itemIdentifier.ToIdentifier());
2275 
2276  Vector2 pos = Vector2.Zero;
2277  Submarine sub = null;
2278  float rotation = 0.0f;
2279  int itemContainerIndex = -1;
2280  int inventorySlotIndex = -1;
2281 
2282  if (inventoryId > 0)
2283  {
2284  itemContainerIndex = msg.ReadByte();
2285  inventorySlotIndex = msg.ReadByte();
2286  }
2287  else
2288  {
2289  pos = new Vector2(msg.ReadSingle(), msg.ReadSingle());
2290  rotation = msg.ReadRangedSingle(0, MathHelper.TwoPi, 8);
2291  ushort subID = msg.ReadUInt16();
2292  if (subID > 0)
2293  {
2294  sub = Submarine.Loaded.Find(s => s.ID == subID);
2295  }
2296  }
2297 
2298  byte bodyType = msg.ReadByte();
2299  bool spawnedInOutpost = msg.ReadBoolean();
2300  bool allowStealing = msg.ReadBoolean();
2301  int quality = msg.ReadRangedInteger(0, Items.Components.Quality.MaxQuality);
2302  byte teamID = msg.ReadByte();
2303 
2304  bool hasIdCard = msg.ReadBoolean();
2305  string ownerName = "", ownerTags = "";
2306  int ownerBeardIndex = -1, ownerHairIndex = -1, ownerMoustacheIndex = -1, ownerFaceAttachmentIndex = -1;
2307  Color ownerHairColor = Color.White,
2308  ownerFacialHairColor = Color.White,
2309  ownerSkinColor = Color.White;
2310  Identifier ownerJobId = Identifier.Empty;
2311  Vector2 ownerSheetIndex = Vector2.Zero;
2312  int submarineSpecificId = 0;
2313  if (hasIdCard)
2314  {
2315  submarineSpecificId = msg.ReadInt32();
2316  ownerName = msg.ReadString();
2317  ownerTags = msg.ReadString();
2318  ownerBeardIndex = msg.ReadByte() - 1;
2319  ownerHairIndex = msg.ReadByte() - 1;
2320  ownerMoustacheIndex = msg.ReadByte() - 1;
2321  ownerFaceAttachmentIndex = msg.ReadByte() - 1;
2322  ownerHairColor = msg.ReadColorR8G8B8();
2323  ownerFacialHairColor = msg.ReadColorR8G8B8();
2324  ownerSkinColor = msg.ReadColorR8G8B8();
2325  ownerJobId = msg.ReadIdentifier();
2326 
2327  int x = msg.ReadByte();
2328  int y = msg.ReadByte();
2329  ownerSheetIndex = (x, y);
2330  }
2331 
2332  bool tagsChanged = msg.ReadBoolean();
2333  string tags = "";
2334  if (tagsChanged)
2335  {
2336  HashSet<Identifier> addedTags = msg.ReadString().Split(',').ToIdentifiers().ToHashSet();
2337  HashSet<Identifier> removedTags = msg.ReadString().Split(',').ToIdentifiers().ToHashSet();
2338  if (itemPrefab != null)
2339  {
2340  tags = string.Join(',',itemPrefab.Tags.Where(t => !removedTags.Contains(t)).Concat(addedTags));
2341  }
2342  }
2343 
2344  bool isNameTag = msg.ReadBoolean();
2345  string writtenName = "";
2346  if (isNameTag)
2347  {
2348  writtenName = msg.ReadString();
2349  }
2350 
2351  if (!spawn) { return null; }
2352 
2353  //----------------------------------------
2354 
2355  if (itemPrefab == null)
2356  {
2357  string errorMsg = "Failed to spawn item, prefab not found (name: " + (itemName ?? "null") + ", identifier: " + (itemIdentifier ?? "null") + ")";
2358  errorMsg += "\n" + string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(cp => cp.Name));
2359  GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:PrefabNotFound" + (itemName ?? "null") + (itemIdentifier ?? "null"),
2360  GameAnalyticsManager.ErrorSeverity.Critical,
2361  errorMsg);
2362  DebugConsole.ThrowError(errorMsg);
2363  return null;
2364  }
2365 
2366  Inventory inventory = null;
2367  if (inventoryId > 0)
2368  {
2369  var inventoryOwner = FindEntityByID(inventoryId);
2370  if (inventoryOwner is Character character)
2371  {
2372  inventory = character.Inventory;
2373  }
2374  else if (inventoryOwner is Item parentItem)
2375  {
2376  if (itemContainerIndex < 0 || itemContainerIndex >= parentItem.components.Count)
2377  {
2378  string errorMsg =
2379  $"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{parentItem.Prefab.Identifier} ({parentItem.ID})\" (component index out of range). Index: {itemContainerIndex}, components: {parentItem.components.Count}.";
2380  GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:ContainerIndexOutOfRange" + (itemName ?? "null") + (itemIdentifier ?? "null"),
2381  GameAnalyticsManager.ErrorSeverity.Error,
2382  errorMsg);
2383  DebugConsole.ThrowError(errorMsg);
2384  inventory = parentItem.GetComponent<ItemContainer>()?.Inventory;
2385  }
2386  else if (parentItem.components[itemContainerIndex] is ItemContainer container)
2387  {
2388  inventory = container.Inventory;
2389  }
2390  }
2391  else if (inventoryOwner == null)
2392  {
2393  DebugConsole.ThrowError($"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of an entity with the ID {inventoryId} (entity not found)");
2394  }
2395  else
2396  {
2397  DebugConsole.ThrowError($"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{inventoryOwner} ({inventoryOwner.ID})\" (invalid entity, should be an item or a character)");
2398  }
2399  }
2400 
2401  Item item = null;
2402  try
2403  {
2404  item = new Item(itemPrefab, pos, sub, id: itemId)
2405  {
2406  SpawnedInCurrentOutpost = spawnedInOutpost,
2407  AllowStealing = allowStealing,
2408  Quality = quality
2409  };
2410  }
2411  catch (Exception e)
2412  {
2413  DebugConsole.ThrowError($"Failed to spawn item {itemPrefab.Name}", e);
2414  throw;
2415  }
2416 
2417  if (item.body != null)
2418  {
2419  item.body.BodyType = (BodyType)bodyType;
2420  }
2421 
2422  foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
2423  {
2424  wifiComponent.TeamID = (CharacterTeamType)teamID;
2425  }
2426  foreach (IdCard idCard in item.GetComponents<IdCard>())
2427  {
2428  idCard.SubmarineSpecificID = submarineSpecificId;
2429  idCard.TeamID = (CharacterTeamType)teamID;
2430  idCard.OwnerName = ownerName;
2431  idCard.OwnerTags = ownerTags;
2432  idCard.OwnerBeardIndex = ownerBeardIndex;
2433  idCard.OwnerHairIndex = ownerHairIndex;
2434  idCard.OwnerMoustacheIndex = ownerMoustacheIndex;
2435  idCard.OwnerFaceAttachmentIndex = ownerFaceAttachmentIndex;
2436  idCard.OwnerHairColor = ownerHairColor;
2437  idCard.OwnerFacialHairColor = ownerFacialHairColor;
2438  idCard.OwnerSkinColor = ownerSkinColor;
2439  idCard.OwnerJobId = ownerJobId;
2440  idCard.OwnerSheetIndex = ownerSheetIndex;
2441  }
2442  if (descriptionChanged) { item.Description = itemDesc; }
2443  if (tagsChanged) { item.Tags = tags; }
2444  var nameTag = item.GetComponent<NameTag>();
2445  if (nameTag != null)
2446  {
2447  nameTag.WrittenName = writtenName;
2448  }
2449 
2450  if (sub != null)
2451  {
2452  item.CurrentHull = Hull.FindHull(pos + sub.Position, null, true);
2453  item.Submarine = item.CurrentHull?.Submarine;
2454  }
2455 
2456  if (inventory != null)
2457  {
2458  if (inventorySlotIndex >= 0 && inventorySlotIndex < 255 &&
2459  inventory.TryPutItem(item, inventorySlotIndex, false, false, null, false))
2460  {
2461  return item;
2462  }
2463  inventory.TryPutItem(item, null, item.AllowedSlots, false);
2464  }
2465 
2466  return item;
2467  }
2468 
2469  partial void RemoveProjSpecific()
2470  {
2471  if (Inventory.DraggingItems.Contains(this))
2472  {
2473  Inventory.DraggingItems.Clear();
2474  Inventory.DraggingSlot = null;
2475  }
2476  }
2477 
2479  {
2480  foreach (ItemComponent ic in components)
2481  {
2482  ic.OnPlayerSkillsChanged();
2483  }
2484  }
2485  }
2486 }
float? Zoom
Definition: Camera.cs:78
static bool BlocksInteraction(InteractionType interactionType)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
readonly DecorativeSpriteBehaviorType DecorativeSpriteBehavior
static void UpdateSpriteStates(ImmutableDictionary< int, ImmutableArray< DecorativeSprite >> spriteGroups, Dictionary< DecorativeSprite, State > animStates, int entityID, float deltaTime, Func< PropertyConditional, bool > checkConditional)
float GetScale(float randomScaleModifier)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
virtual Vector2 DrawPosition
Definition: Entity.cs:51
Submarine Submarine
Definition: Entity.cs:53
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
virtual void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
virtual RichString ToolTip
virtual Rectangle Rect
RectTransform RectTransform
IEnumerable< GUIComponent > Children
Definition: GUIComponent.cs:29
GUIComponent that can be used to render custom content on the UI
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
List< GUIButton > Buttons
static void AutoScaleAndNormalize(params GUITextBlock[] textBlocks)
Set the text scale of the GUITextBlocks so that they all use the same scale and can fit the text with...
TextBoxEvent OnDeselected
Definition: GUITextBox.cs:17
OnEnterHandler OnEnterPressed
Definition: GUITextBox.cs:29
override bool Selected
Definition: GUITickBox.cs:18
static int GraphicsWidth
Definition: GameMain.cs:162
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static int GraphicsHeight
Definition: GameMain.cs:168
static GameScreen GameScreen
Definition: GameMain.cs:52
static bool DebugDraw
Definition: GameMain.cs:29
static GameClient Client
Definition: GameMain.cs:188
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)
Inventory(Entity owner, int capacity, int slotsPerRow=5)
List< ColoredText > GetHUDTexts(Character character, bool recreateHudTexts=true)
override Quad2D GetTransformedQuad()
override bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent=false)
Adds a new upgrade to the item
void CreateContainerTagPicker([MaybeNull] GUITextBox tagTextBox)
override bool IsMouseOn(Vector2 position)
void DrawDecorativeSprites(SpriteBatch spriteBatch, Vector2 drawPos, bool flipX, bool flipY, float rotation, float depth, Color? overrideColor=null)
bool ConditionalMatches(PropertyConditional conditional)
IEnumerable< ItemComponent > ActiveHUDs
Color GetSpriteColor(Color? defaultColor=null, bool withHighlight=false)
override void AddToGUIUpdateList(int order=0)
void ClientEventRead(IReadMessage msg, float sendingTime)
void DrawHUD(SpriteBatch spriteBatch, Camera cam, Character character)
override void FlipX(bool relativeToSub)
Flip the entity horizontally
Rectangle TransformTrigger(Rectangle trigger, bool world=false)
override void FlipY(bool relativeToSub)
Flip the entity vertically
static Item ReadSpawnData(IReadMessage msg, bool spawn=true)
void ForceHUDLayoutUpdate(bool ignoreLocking=false)
float ConditionPercentageRelativeToDefaultMaxCondition
Condition percentage disregarding MaxRepairConditionMultiplier (i.e. this can go above 100% if the it...
override void Draw(SpriteBatch spriteBatch, bool editing, bool back=true)
void UpdateHUD(Camera cam, Character character, float deltaTime)
override bool IsVisible(Rectangle worldView)
void ClientReadPosition(IReadMessage msg, float sendingTime)
bool HasVisibleInteraction(Character character)
void CheckNeedsSoundUpdate(ItemComponent ic)
Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id=Entity.NullEntityID, bool callOnItemLoaded=true)
void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData=null)
override void UpdateEditing(Camera cam, float deltaTime)
void Draw(SpriteBatch spriteBatch, bool editing, bool back=true, Color? overrideColor=null)
GUIComponent CreateEditingHUD(bool inGame=false)
ImmutableArray< ContainedItemSprite > ContainedSprites
static ItemPrefab Find(string name, Identifier identifier)
override ImmutableHashSet< Identifier > Tags
ImmutableArray< BrokenItemSprite > BrokenSprites
The base class for components holding the different functionalities of the item
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)
virtual void DrawHUD(SpriteBatch spriteBatch, Character character)
Dictionary< RelatedItem.RelationType, List< RelatedItem > > RequiredItems
readonly List< WearableSprite > WearingItems
virtual void FlipX(bool relativeToSub)
Flip the entity horizontally
virtual void FlipY(bool relativeToSub)
Flip the entity vertically
static void ColorFlipButton(GUIButton btn, bool flip)
bool IsHidden
Is the entity hidden due to HiddenInGame being enabled or the layer the entity is in being hidden?
static readonly HashSet< MapEntity > highlightedEntities
readonly List< Upgrade > Upgrades
List of upgrades this item has
override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData=null)
Definition: GameClient.cs:2606
void DebugDraw(SpriteBatch spriteBatch, Color color, bool forceColor=false)
void Draw(DeformableSprite deformSprite, Camera cam, Vector2 scale, Color color, bool invert=false)
static bool KeyDown(InputType inputType)
float GetDepthOffset()
Offset added to the default draw depth of the character's limbs. For example, climbing on ladders aff...
Limb GetLimb(LimbType limbType, bool excludeSevered=true)
Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
Point ScreenSpaceOffset
Screen space offset. From top left corner. In pixels.
Point?? MinSize
Min size in pixels. Does not affect scaling.
Point NonScaledSize
Size before scale multiplications.
bool IsFixedSize
If false, the element will resize if the parent is resized (with the children). If true,...
static RichString Rich(LocalizedString str, Func< string, string >? postProcess=null)
Definition: RichString.cs:67
static Dictionary< Identifier, SerializableProperty > GetProperties(object obj)
void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate=0.0f, float scale=1.0f, SpriteEffects spriteEffect=SpriteEffects.None)
Sprite(ContentXElement element, string path="", string file="", bool lazyLoad=false, float sourceRectScale=1)
void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, float rotation=0f, Vector2? origin=null, Color? color=null, Vector2? startOffset=null, Vector2? textureScale=null, float? depth=null, SpriteEffects? spriteEffects=null)
void ReloadXML()
Works only if there is a name attribute defined for the sprite. For items and structures,...
static bool IsLayerVisible(MapEntity entity)
bool IsSubcategoryHidden(string subcategory)
static bool IsWiringMode()
List< Item > GetItems(bool alsoFromConnectedSubs)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
bool IsEditable(ISerializableEntity entity)
Vector2 DrawSize
The extents of the sprites or other graphics this component needs to draw. Used to determine which it...
Interface for entities that the clients can send events to the server
Microsoft.Xna.Framework.Color ReadColorR8G8B8A8()
int ReadRangedInteger(int min, int max)
Single ReadRangedSingle(Single min, Single max, int bitCount)
Microsoft.Xna.Framework.Color ReadColorR8G8B8()
Interface for entities that the server can send events to the clients
void WriteRangedInteger(int val, int min, int max)
CursorState
Definition: GUI.cs:40
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19