Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs
1 using FarseerPhysics;
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Xml.Linq;
8 
10 {
12  {
13  [Editable]
14  public LimbType LimbType { get; set; }
15 
16  [Editable]
17  public Vector2 Position { get; set; }
18 
19  public bool AllowUsingLimb;
20 
21  public string Name => LimbType.ToString();
22 
23  public Dictionary<Identifier, SerializableProperty> SerializableProperties => null;
24 
25  public LimbPos(LimbType limbType, Vector2 position, bool allowUsingLimb)
26  {
27  LimbType = limbType;
28  Position = position;
29  AllowUsingLimb = allowUsingLimb;
30  }
31  }
32 
34  {
35  //where the limbs of the user should be positioned when using the controller
36  private readonly List<LimbPos> limbPositions = new List<LimbPos>();
37 
38  private Direction dir;
39  public Direction Direction => dir;
40 
41  //the position where the user walks to when using the controller
42  //(relative to the position of the item)
43  private Vector2 userPos;
44 
45  private Camera cam;
46 
47  private Character user;
48 
49  private Item focusTarget;
50  private float targetRotation;
51 
52  public Vector2 UserPos
53  {
54  get { return userPos; }
55  set { userPos = value; }
56  }
57 
58  public Character User
59  {
60  get { return user; }
61  }
62 
63  public IEnumerable<LimbPos> LimbPositions { get { return limbPositions; } }
64 
65  [Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out a signal when interacted with.", alwaysUseInstanceValues: true)]
66  public bool IsToggle
67  {
68  get;
69  set;
70  }
71 
72  private string output;
73  [ConditionallyEditable(ConditionallyEditable.ConditionType.HasConnectionPanel, onlyInEditors: false),
74  Serialize("1", IsPropertySaveable.Yes, description: "The signal sent when the controller is being activated or is toggled on. If empty, no signal is sent.", alwaysUseInstanceValues: true)]
75  public string Output
76  {
77  get { return output; }
78  set
79  {
80  if (value == null || value == output) { return; }
81  output = value;
82  //reactivate if signal isn't empty (we may not have been previously sending a signal, but might now)
83  if (!value.IsNullOrEmpty()) { IsActive = true; }
84  }
85  }
86 
87  private string falseOutput;
88  [ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: false),
89  Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the controller is toggled off. If empty, no signal is sent. Only valid if IsToggle is true.", alwaysUseInstanceValues: true)]
90  public string FalseOutput
91  {
92  get { return falseOutput; }
93  set
94  {
95  if (value == null || value == falseOutput) { return; }
96  falseOutput = value;
97  //reactivate if signal isn't empty (we may not have been previously sending a signal, but might now)
98  if (!value.IsNullOrEmpty()) { IsActive = true; }
99  }
100  }
101 
102  private bool state;
103  [ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: true),
104  Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)]
105  public bool State
106  {
107  get { return state; }
108  set
109  {
110  if (state != value)
111  {
112  state = value;
113  string newOutput = state ? output : falseOutput;
114  IsActive = !string.IsNullOrEmpty(newOutput);
115  }
116  }
117  }
118 
119  [Serialize(true, IsPropertySaveable.No, description: "Should the HUD (inventory, health bar, etc) be hidden when this item is selected.")]
120  public bool HideHUD
121  {
122  get;
123  set;
124  }
125 
126  public enum UseEnvironment
127  {
128  Air, Water, Both
129  };
130 
131  [Serialize(UseEnvironment.Both, IsPropertySaveable.No, description: "Can the item be selected in air, underwater or both.")]
132  public UseEnvironment UsableIn { get; set; }
133 
134  [Serialize(false, IsPropertySaveable.No, description: "Should the character using the item be drawn behind the item.")]
135  public bool DrawUserBehind
136  {
137  get;
138  set;
139  }
140 
141  [Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when another character has already selected it?")]
143  {
144  get;
145  set;
146  }
147 
148  [Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when a bot has already selected it?")]
150  {
151  get;
152  set;
153  }
154 
156  {
157  get { return limbPositions.Count > 0; }
158  }
159 
161  {
162  get;
163  private set;
164  }
165 
166  public bool AllowAiming
167  {
168  get;
169  private set;
170  } = true;
171 
172  [Serialize(false, IsPropertySaveable.No)]
174  {
175  get;
176  set;
177  }
178 
179  [Serialize(false, IsPropertySaveable.No)]
181  {
182  get;
183  set;
184  }
185 
186  [Serialize(false, IsPropertySaveable.No, description: "If true, other items can be used simultaneously.")]
187  public bool IsSecondaryItem
188  {
189  get;
190  private set;
191  }
192 
194  : base(item, element)
195  {
196  userPos = element.GetAttributeVector2("UserPos", Vector2.Zero);
197  Enum.TryParse(element.GetAttributeString("direction", "None"), out dir);
198  LoadLimbPositions(element);
199  IsActive = true;
200  }
201 
202  public override void Update(float deltaTime, Camera cam)
203  {
204  this.cam = cam;
205  UserInCorrectPosition = false;
206 
207  string signal = IsToggle && State ? output : falseOutput;
208  if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal))
209  {
210  item.SendSignal(signal, "signal_out");
211  item.SendSignal(signal, "trigger_out");
212  }
213 
214  if (user == null
215  || user.Removed
216  || !user.IsAnySelectedItem(item)
217  || item.ParentInventory != null
218  || !user.CanInteractWith(item)
219  || (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
220  || (UsableIn == UseEnvironment.Air && user.AnimController.InWater))
221  {
222  if (user != null)
223  {
224  CancelUsing(user);
225  user = null;
226  }
227  if (item.Connections == null || !IsToggle || string.IsNullOrEmpty(signal)) { IsActive = false; }
228  return;
229  }
230 
232 
233  if (userPos != Vector2.Zero)
234  {
235  Vector2 diff = (item.WorldPosition + userPos) - user.WorldPosition;
236 
237  if (user.AnimController.InWater)
238  {
239  if (diff.LengthSquared() > 30.0f * 30.0f)
240  {
241  user.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One);
242  user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
243  }
244  else
245  {
246  user.AnimController.TargetMovement = Vector2.Zero;
247  UserInCorrectPosition = true;
248  }
249  }
250  else
251  {
252  // Secondary items (like ladders or chairs) will control the character position over primary items
253  // Only control the character position if the character doesn't have another secondary item already controlling it
255  {
256  diff.Y = 0.0f;
257  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && user != Character.Controlled)
258  {
259  if (Math.Abs(diff.X) > 20.0f)
260  {
261  //wait for the character to walk to the correct position
262  return;
263  }
264  else if (Math.Abs(diff.X) > 0.1f)
265  {
266  //aim to keep the collider at the correct position once close enough
267  user.AnimController.Collider.LinearVelocity = new Vector2(
268  diff.X * 0.1f,
270  }
271  }
272  else if (Math.Abs(diff.X) > 10.0f)
273  {
274  user.AnimController.TargetMovement = Vector2.Normalize(diff);
275  user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
276  return;
277  }
278  user.AnimController.TargetMovement = Vector2.Zero;
279  }
280  UserInCorrectPosition = true;
281  }
282  }
283 
284  ApplyStatusEffects(ActionType.OnActive, deltaTime, user);
285 
286  if (limbPositions.Count == 0) { return; }
287 
289 
290  if (user.SelectedItem != null)
291  {
292  user.AnimController.ResetPullJoints(l => l.IsLowerBody);
293  }
294  else
295  {
297  }
298 
299  if (dir != 0) { user.AnimController.TargetDir = dir; }
300 
301  foreach (LimbPos lb in limbPositions)
302  {
303  Limb limb = user.AnimController.GetLimb(lb.LimbType);
304  if (limb == null || !limb.body.Enabled) { continue; }
305  // Don't move lower body limbs if there's another selected secondary item that should control them
306  if (limb.IsLowerBody && user.HasSelectedAnotherSecondaryItem(Item)) { continue; }
307  // Don't move hands if there's a selected primary item that should control them
308  if (!limb.IsLowerBody && Item == user.SelectedSecondaryItem && user.SelectedItem != null) { continue; }
309  if (lb.AllowUsingLimb)
310  {
311  switch (lb.LimbType)
312  {
313  case LimbType.RightHand:
314  case LimbType.RightForearm:
315  case LimbType.RightArm:
316  if (user.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; }
317  break;
318  case LimbType.LeftHand:
319  case LimbType.LeftForearm:
320  case LimbType.LeftArm:
321  if (user.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; }
322  break;
323  }
324  }
325  limb.Disabled = true;
326  Vector2 worldPosition = new Vector2(item.WorldRect.X, item.WorldRect.Y) + lb.Position * item.Scale;
327  Vector2 diff = worldPosition - limb.WorldPosition;
328  limb.PullJointEnabled = true;
329  limb.PullJointWorldAnchorB = limb.SimPosition + ConvertUnits.ToSimUnits(diff);
330  }
331  }
332 
333  private double lastUsed;
334 
335  public override bool Use(float deltaTime, Character activator = null)
336  {
337  if (activator != user)
338  {
339  return false;
340  }
341  if (user == null || user.Removed || !user.IsAnySelectedItem(item) || !user.CanInteractWith(item))
342  {
343  user = null;
344  return false;
345  }
346 
347  if (IsToggle && (activator == null || lastUsed < Timing.TotalTime - 0.1))
348  {
349  if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
350  {
351  State = !State;
352 #if SERVER
353  item.CreateServerEvent(this);
354 #endif
355  }
356  }
357  else if (!string.IsNullOrEmpty(output))
358  {
359  item.SendSignal(new Signal(output, sender: user), "trigger_out");
360  }
361 
362  lastUsed = Timing.TotalTime;
363  ApplyStatusEffects(ActionType.OnUse, 1.0f, activator);
364  return true;
365  }
366 
367  public override bool SecondaryUse(float deltaTime, Character character = null)
368  {
369  if (user != character)
370  {
371  return false;
372  }
373  if (user == null || character.Removed || !user.IsAnySelectedItem(item) || !character.CanInteractWith(item))
374  {
375  user = null;
376  return false;
377  }
378  if (character == null)
379  {
380  return false;
381  }
382 
383  focusTarget = GetFocusTarget();
384 
385  if (focusTarget == null)
386  {
387  Vector2 centerPos = new Vector2(item.WorldRect.Center.X, item.WorldRect.Center.Y);
388  Vector2 offset = character.CursorWorldPosition - centerPos;
389  offset.Y = -offset.Y;
390  targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset));
391  return false;
392  }
393 
394  character.ViewTarget = focusTarget;
395 
396 #if CLIENT
397  if (character == Character.Controlled && cam != null)
398  {
399  Lights.LightManager.ViewTarget = focusTarget;
400  cam.TargetPos = focusTarget.WorldPosition;
401  cam.OffsetAmount = MathHelper.Lerp(cam.OffsetAmount, (focusTarget as Item).Prefab.OffsetOnSelected * focusTarget.OffsetOnSelectedMultiplier, deltaTime * 10.0f);
402  HideHUDs(true);
403  }
404 #endif
405 
406  if (!character.IsRemotePlayer || character.ViewTarget == focusTarget)
407  {
408  Vector2 centerPos = new Vector2(focusTarget.WorldRect.Center.X, focusTarget.WorldRect.Center.Y);
409  if (focusTarget.GetComponent<Turret>() is { } turret)
410  {
411  centerPos = new Vector2(focusTarget.WorldRect.X + turret.TransformedBarrelPos.X, focusTarget.WorldRect.Y - turret.TransformedBarrelPos.Y);
412  }
413  Vector2 offset = character.CursorWorldPosition - centerPos;
414  offset.Y = -offset.Y;
415  targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset));
416  }
417  return true;
418  }
419 
421  {
422  var positionOut = item.Connections?.Find(c => c.Name == "position_out");
423  if (positionOut == null) { return null; }
424 
425  item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut);
426 
427  for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
428  {
429  if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; }
430  if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected)
431  {
432  return item.LastSentSignalRecipients[i].Item;
433  }
434  }
435 
436  foreach (var recipientPanel in item.GetConnectedComponentsRecursive<ConnectionPanel>(positionOut, allowTraversingBackwards: false))
437  {
438  if (recipientPanel.Item.Condition <= 0.0f) { continue; }
439  if (recipientPanel.Item.Prefab.FocusOnSelected)
440  {
441  return recipientPanel.Item;
442  }
443  }
444 
445  return null;
446  }
447 
448  public override bool Pick(Character picker)
449  {
450 #if CLIENT
451  if (Screen.Selected == GameMain.SubEditorScreen) { return false; }
452 #endif
453  if (IsToggle)
454  {
455  if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
456  {
457  State = !State;
458 #if SERVER
459  item.CreateServerEvent(this);
460 #endif
461  }
462  }
463  else if (!string.IsNullOrEmpty(output))
464  {
465  item.SendSignal(new Signal(output, sender: picker), "signal_out");
466  }
467 #if CLIENT
468  PlaySound(ActionType.OnUse, picker);
469 #endif
470  return true;
471  }
472 
473  private void CancelUsing(Character character)
474  {
475  if (character == null || character.Removed) { return; }
476 
477  foreach (LimbPos lb in limbPositions)
478  {
479  Limb limb = character.AnimController.GetLimb(lb.LimbType);
480  if (limb == null) { continue; }
481 
482  limb.Disabled = false;
483  limb.PullJointEnabled = false;
484  }
485 
486  //disable flipping for 0.5 seconds, because flipping the character when it's in a weird pose (e.g. lying in bed) can mess up the ragdoll
487  if (character.AnimController is HumanoidAnimController humanoidAnim)
488  {
489  humanoidAnim.LockFlipping(0.5f);
490  }
491 
492  if (character.SelectedItem == item) { character.SelectedItem = null; }
493  if (character.SelectedSecondaryItem == item) { character.SelectedSecondaryItem = null; }
494 
495  character.AnimController.StopUsingItem();
496  if (character == Character.Controlled)
497  {
498  HideHUDs(false);
499  }
500 #if SERVER
501  item.CreateServerEvent(this);
502 #endif
503  }
504 
505  public override bool Select(Character activator)
506  {
507  if (activator == null || activator.Removed) { return false; }
508  if (Item.Condition <= 0.0f && !UpdateWhenInactive) { return false; }
509 
510  if (UsableIn == UseEnvironment.Water && !activator.AnimController.InWater ||
511  UsableIn == UseEnvironment.Air && activator.AnimController.InWater)
512  {
513  return false;
514  }
515 
516  //someone already using the item
517  if (user != null && !user.Removed)
518  {
519  if (user == activator)
520  {
521  IsActive = false;
522  CancelUsing(user);
523  user = null;
524  return false;
525  }
526  else if (user.IsBot && !activator.IsBot)
527  {
529  {
530  CancelUsing(user);
531  user = activator;
532  IsActive = true;
533  return true;
534  }
535  }
537  }
538  else
539  {
540  user = activator;
541  IsActive = true;
542  }
543 #if SERVER
544  item.CreateServerEvent(this);
545 #endif
546  if (!string.IsNullOrEmpty(output))
547  {
548  item.SendSignal(new Signal(output, sender: user), "signal_out");
549  }
550  return true;
551  }
552 
553  public override void FlipX(bool relativeToSub)
554  {
555  if (dir != Direction.None)
556  {
557  dir = dir == Direction.Left ? Direction.Right : Direction.Left;
558  }
559  userPos.X = -UserPos.X;
560  FlipLimbPositions();
561  }
562 
563  public override void FlipY(bool relativeToSub)
564  {
565  userPos.Y = -UserPos.Y;
566 
567  for (int i = 0; i < limbPositions.Count; i++)
568  {
569  float diff = (item.Rect.Y + limbPositions[i].Position.Y) - item.Rect.Center.Y;
570 
571  Vector2 flippedPos =
572  new Vector2(
573  limbPositions[i].Position.X,
574  item.Rect.Center.Y - diff - item.Rect.Y);
575  limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb);
576  }
577  }
578 
579  public override bool HasAccess(Character character)
580  {
581  if (!item.IsInteractable(character)) { return false; }
582  return base.HasAccess(character);
583  }
584 
585  partial void HideHUDs(bool value);
586 
587  public override XElement Save(XElement parentElement)
588  {
589  return SaveLimbPositions(base.Save(parentElement));
590  }
591 
592  public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
593  {
594  base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
596  {
597  LoadLimbPositions(componentElement);
598  }
599  }
600 
601  private XElement SaveLimbPositions(XElement element)
602  {
604  {
605  if (item.FlippedX)
606  {
607  FlipLimbPositions();
608  }
609  // Don't save flipped positions.
610  foreach (var limbPos in limbPositions)
611  {
612  element.Add(new XElement("limbposition",
613  new XAttribute("limb", limbPos.LimbType),
614  new XAttribute("position", XMLExtensions.Vector2ToString(limbPos.Position)),
615  new XAttribute("allowusinglimb", limbPos.AllowUsingLimb)));
616  }
617  if (item.FlippedX)
618  {
619  FlipLimbPositions();
620  }
621  }
622  return element;
623  }
624 
625  private void LoadLimbPositions(ContentXElement element)
626  {
627  limbPositions.Clear();
628  foreach (var subElement in element.Elements())
629  {
630  if (subElement.Name != "limbposition") { continue; }
631  string limbStr = subElement.GetAttributeString("limb", "");
632  if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType))
633  {
634  DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type.",
635  contentPackage: element.ContentPackage);
636  }
637  else
638  {
639  LimbPos limbPos = new LimbPos(limbType,
640  subElement.GetAttributeVector2("position", Vector2.Zero),
641  subElement.GetAttributeBool("allowusinglimb", false));
642  limbPositions.Add(limbPos);
643  if (!limbPos.AllowUsingLimb)
644  {
645  if (limbType == LimbType.RightHand || limbType == LimbType.RightForearm || limbType == LimbType.RightArm ||
646  limbType == LimbType.LeftHand || limbType == LimbType.LeftForearm || limbType == LimbType.LeftArm)
647  {
648  AllowAiming = false;
649  }
650  }
651  }
652  }
653  }
654 
655  private void FlipLimbPositions()
656  {
657  for (int i = 0; i < limbPositions.Count; i++)
658  {
659  float diff = (item.Rect.X + limbPositions[i].Position.X * item.Scale) - item.Rect.Center.X;
660 
661  Vector2 flippedPos =
662  new Vector2(
663  (item.Rect.Center.X - diff - item.Rect.X) / item.Scale,
664  limbPositions[i].Position.Y);
665  limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb);
666  }
667  }
668 
669  public override void OnItemLoaded()
670  {
672  {
673  item.NonInteractable = true;
674  }
676  {
677  item.NonInteractable = true;
678  }
679  }
680 
681  public override void Reset()
682  {
683  base.Reset();
684  LoadLimbPositions(originalElement);
685  if (item.FlippedX)
686  {
687  FlipLimbPositions();
688  }
689  }
690  }
691 }
float OffsetAmount
Definition: Camera.cs:118
Vector2 TargetPos
Definition: Camera.cs:156
bool CanInteractWith(Character c, float maxDist=200.0f, bool checkVisibility=true, bool skipDistanceCheck=false)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
bool HasSelectedAnotherSecondaryItem(Item item)
Item SelectedSecondaryItem
The secondary selected item. It's an item other than a device (see SelectedItem), e....
bool IsAnySelectedItem(Item item)
Is the item either the primary or the secondary selected item?
string? GetAttributeString(string key, string? def)
Vector2 GetAttributeVector2(string key, in Vector2 def)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
static GameSession?? GameSession
Definition: GameMain.cs:88
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static GameModePreset TestMode
bool IsInteractable(Character character)
Returns interactibility based on whether the character is on a player team
List< Connection > LastSentSignalRecipients
A list of connections the last signal sent by this item went through
void SendSignal(string signal, string connectionName)
override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
override bool HasAccess(Character character)
Only checks if any of the Picked requirements are matched (used for checking id card(s))....
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
override bool Pick(Character picker)
a Character has picked the item
The base class for components holding the different functionalities of the item
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)
LimbPos(LimbType limbType, Vector2 position, bool allowUsingLimb)
void ResetPullJoints(Func< Limb, bool > condition=null)
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.
Interface for entities that the server can send events to the clients
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19