Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/DockingPort.cs
1 using Barotrauma.IO;
3 using FarseerPhysics;
4 using FarseerPhysics.Dynamics;
5 using FarseerPhysics.Dynamics.Joints;
6 using Microsoft.Xna.Framework;
7 using System;
8 using System.Collections.Generic;
9 using System.Linq;
10 #if CLIENT
11 using Barotrauma.Lights;
12 #endif
13 
15 {
17  {
18  public enum DirectionType
19  {
20  None,
21  Top,
22  Bottom,
23  Left,
24  Right
25  }
26 
27  private static readonly List<DockingPort> list = new List<DockingPort>();
28  public static IEnumerable<DockingPort> List
29  {
30  get { return list; }
31  }
32 
33  private Sprite overlaySprite;
34  private float dockingState;
35  private Joint joint;
36 
37  private readonly Hull[] hulls = new Hull[2];
38  private Gap gap;
39  private Body[] bodies;
40  private Fixture outsideBlocker;
41  private Body doorBody;
42 
43  private float dockingCooldown;
44 
45  private bool docked;
46  private bool obstructedWayPointsDisabled;
47 
48  private float forceLockTimer;
49  //if the submarine isn't in the correct position to lock within this time after docking has been activated,
50  //force the sub to the correct position
51  const float ForceLockDelay = 1.0f;
52 
53  public int DockingDir { get; set; }
54 
55  [Serialize("32.0,32.0", IsPropertySaveable.No, description: "How close the docking port has to be to another port to dock.")]
56  public Vector2 DistanceTolerance { get; set; }
57 
58  [Serialize(32.0f, IsPropertySaveable.No, description: "How close together the docking ports are forced when docked.")]
59  public float DockedDistance
60  {
61  get;
62  set;
63  }
64 
65  [Serialize(true, IsPropertySaveable.No, description: "Is the port horizontal.")]
66  public bool IsHorizontal
67  {
68  get;
69  set;
70  }
71 
72  [Editable, Serialize(false, IsPropertySaveable.Yes, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")]
73  public bool MainDockingPort
74  {
75  get;
76  set;
77  }
78 
79  [Editable, Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")]
81  {
82  get;
83  set;
84  }
85 
86  [Editable, Serialize(DirectionType.None, IsPropertySaveable.No, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n" +
87  "Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")]
88  public DirectionType ForceDockingDirection { get; set; }
89 
90  public DockingPort DockingTarget { get; private set; }
91 
95  public bool AtStartExit => Item.Submarine is { AtStartExit: true};
96  public bool AtEndExit => Item.Submarine is { AtEndExit: true };
97 
98  public Door Door { get; private set; }
99 
100  public bool Docked
101  {
102  get
103  {
104  return docked;
105  }
106  set
107  {
108  if (!docked && value)
109  {
110  if (DockingTarget == null) { AttemptDock(); }
111  if (DockingTarget == null) { return; }
112 
113  docked = true;
114  }
115  else if (docked && !value)
116  {
117  Undock();
118  }
119  }
120  }
121 
122  public bool IsLocked
123  {
124  get { return joint is WeldJoint || DockingTarget?.joint is WeldJoint; }
125  }
126 
127  public bool AnotherPortInProximity => FindAdjacentPort() != null;
128 
132  public event Action OnDocked;
133 
137  public event Action OnUnDocked;
138 
139  private bool outpostAutoDockingPromptShown;
140 
141  enum AllowOutpostAutoDocking
142  {
143  Ask, Yes, No
144  }
145  private AllowOutpostAutoDocking allowOutpostAutoDocking = AllowOutpostAutoDocking.Ask;
146 
148  : base(item, element)
149  {
150  // isOpen = false;
151  foreach (var subElement in element.Elements())
152  {
153  string texturePath = subElement.GetAttributeString("texture", "");
154  switch (subElement.Name.ToString().ToLowerInvariant())
155  {
156  case "sprite":
157  overlaySprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath));
158  break;
159  }
160  }
161 
162  IsActive = true;
163 
164  list.Add(this);
165  }
166 
167  public override void FlipX(bool relativeToSub)
168  {
169  if (DockingTarget != null)
170  {
171  if (IsHorizontal)
172  {
173  DockingDir = 0;
176  }
177 
178  //undock and redock to recreate the hulls, gaps and physics bodies
179  var prevDockingTarget = DockingTarget;
180  Undock(applyEffects: false);
181  Dock(prevDockingTarget);
182  Lock(isNetworkMessage: true, applyEffects: false);
183  }
184  }
185 
186  public override void FlipY(bool relativeToSub)
187  {
188  FlipX(relativeToSub);
189  }
190 
191  private DockingPort FindAdjacentPort()
192  {
193  float closestDist = float.MaxValue;
194  DockingPort closestPort = null;
195  foreach (DockingPort port in list)
196  {
197  if (port == this || port.item.Submarine == item.Submarine || port.IsHorizontal != IsHorizontal) { continue; }
198  float xDist = Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X);
199  if (xDist > DistanceTolerance.X) { continue; }
200  float yDist = Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y);
201  if (yDist > DistanceTolerance.Y) { continue; }
202 
203  float dist = xDist + yDist;
204  //disfavor non-interactable ports
205  if (port.item.NonInteractable) { dist *= 2; }
206  if (dist < closestDist)
207  {
208  closestPort = port;
209  closestDist = dist;
210  }
211  }
212  return closestPort;
213  }
214 
215  private void AttemptDock()
216  {
217  var adjacentPort = FindAdjacentPort();
218  if (adjacentPort != null) { Dock(adjacentPort); }
219  }
220 
221  public void Dock(DockingPort target)
222  {
223  if (item.Submarine.DockedTo.Contains(target.item.Submarine)) { return; }
224 
225  forceLockTimer = 0.0f;
226  dockingCooldown = 0.1f;
227 
228  if (DockingTarget != null)
229  {
230  Undock();
231  }
232 
233  if (target.item.Submarine == item.Submarine)
234  {
235  DebugConsole.ThrowError("Error - tried to dock a submarine to itself");
236  DockingTarget = null;
237  return;
238  }
239 
240  target.InitializeLinks();
241 
242  if (!item.linkedTo.Contains(target.item)) { item.linkedTo.Add(target.item); }
243  if (!target.item.linkedTo.Contains(item)) { target.item.linkedTo.Add(item); }
244 
245  if (!target.item.Submarine.DockedTo.Contains(item.Submarine))
246  {
247  target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target);
249  }
250  if (!item.Submarine.DockedTo.Contains(target.item.Submarine))
251  {
254  }
255 
256  DockingTarget = target;
258 
259  docked = true;
260  DockingTarget.Docked = true;
261 
262  if (Character.Controlled != null &&
264  {
266  }
267 
270 
271  CreateJoint(false);
272 
273 #if SERVER
274  if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
275  {
276  item.CreateServerEvent(this);
277  }
278 #endif
279 
280  OnDocked?.Invoke();
281  OnDocked = null;
282  }
283 
284  public void Lock(bool isNetworkMessage, bool applyEffects = true)
285  {
286 #if CLIENT
287  if (GameMain.Client != null && !isNetworkMessage) { return; }
288 #endif
289 
290  if (DockingTarget == null)
291  {
292  DebugConsole.ThrowError("Error - attempted to lock a docking port that's not connected to anything");
293  return;
294  }
295 
296  if (joint == null)
297  {
298  string errorMsg = "Error while locking a docking port (joint between submarines doesn't exist)." +
299  " Submarine: " + (item.Submarine?.Info.Name ?? "null") +
300  ", target submarine: " + (DockingTarget.item.Submarine?.Info.Name ?? "null");
301  GameAnalyticsManager.AddErrorEventOnce("DockingPort.Lock:JointNotCreated", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
302  return;
303  }
304 
305  if (joint is not WeldJoint)
306  {
309 
310  if (applyEffects && ApplyEffectsOnDocking)
311  {
312  ApplyStatusEffects(ActionType.OnUse, 1.0f);
313  }
314 
315  Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
318  {
319  item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff));
320  }
323  {
324  DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
325  }
326 
327  ConnectWireBetweenPorts();
328  CreateJoint(true);
329  item.SendSignal("1", "on_dock");
330  DockingTarget.Item.SendSignal("1", "on_dock");
331 
332 #if SERVER
333  if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
334  {
335  item.CreateServerEvent(this);
336  }
337 #else
338  if (GameMain.Client != null && GameMain.Client.MidRoundSyncing &&
340  {
342  }
343 #endif
344  }
345 
346 
347  List<MapEntity> removedEntities = item.linkedTo.Where(e => e.Removed).ToList();
348  foreach (MapEntity removed in removedEntities) { item.linkedTo.Remove(removed); }
349 
350  if (!item.linkedTo.Any(e => e is Hull) && !DockingTarget.item.linkedTo.Any(e => e is Hull))
351  {
352  CreateHulls();
353  }
354 
355  if (Door != null && DockingTarget.Door != null)
356  {
357  WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => Door.LinkedGap == wp.ConnectedGap);
358  WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.Door.LinkedGap == wp.ConnectedGap);
359 
360  if (myWayPoint != null && targetWayPoint != null)
361  {
362  myWayPoint.FindHull();
363  targetWayPoint.FindHull();
364  myWayPoint.ConnectTo(targetWayPoint);
365  }
366  }
367  }
368 
369 
370  private void CreateJoint(bool useWeldJoint)
371  {
372  if (joint != null)
373  {
374  GameMain.World.Remove(joint);
375  joint = null;
376  }
377 
378  Vector2 offset = IsHorizontal ?
379  Vector2.UnitX * DockingDir :
380  Vector2.UnitY * DockingDir;
381  offset *= DockedDistance * 0.5f * item.Scale;
382 
383  Vector2 pos1 = item.WorldPosition + offset;
384  Vector2 pos2 = DockingTarget.item.WorldPosition - offset;
385 
386  if (useWeldJoint)
387  {
388  joint = JointFactory.CreateWeldJoint(GameMain.World,
390  ConvertUnits.ToSimUnits(pos1), ConvertUnits.ToSimUnits(pos2), true);
391 
392  ((WeldJoint)joint).FrequencyHz = 1.0f;
393  joint.CollideConnected = false;
394  }
395  else
396  {
397  var distanceJoint = JointFactory.CreateDistanceJoint(GameMain.World,
399  ConvertUnits.ToSimUnits(pos1), ConvertUnits.ToSimUnits(pos2), true);
400 
401  distanceJoint.Length = 0.01f;
402  distanceJoint.Frequency = 1.0f;
403  distanceJoint.DampingRatio = 0.8f;
404 
405  joint = distanceJoint;
406  joint.CollideConnected = true;
407  }
408  }
409 
410  public int GetDir(DockingPort dockingTarget = null)
411  {
412  int forcedDockingDir = GetForcedDockingDir();
413  if (forcedDockingDir != 0) { return forcedDockingDir; }
414  if (dockingTarget != null)
415  {
416  forcedDockingDir = -dockingTarget.GetForcedDockingDir();
417  if (forcedDockingDir != 0) { return forcedDockingDir; }
418  }
419 
420  if (DockingDir != 0) { return DockingDir; }
421 
422  if (Door != null && Door.LinkedGap.linkedTo.Count > 0)
423  {
424  Hull refHull = null;
425  float largestHullSize = 0.0f;
426  foreach (MapEntity linked in Door.LinkedGap.linkedTo)
427  {
428  if (!(linked is Hull hull)) { continue; }
429  if (hull.Volume > largestHullSize)
430  {
431  refHull = hull;
432  largestHullSize = hull.Volume;
433  }
434  }
435  if (refHull != null)
436  {
437  return IsHorizontal ?
438  Math.Sign(Door.Item.WorldPosition.X - refHull.WorldPosition.X) :
439  Math.Sign(Door.Item.WorldPosition.Y - refHull.WorldPosition.Y);
440  }
441  }
442  if (dockingTarget?.Door?.LinkedGap != null && dockingTarget.Door.LinkedGap.linkedTo.Count > 0)
443  {
444  Hull refHull = null;
445  float largestHullSize = 0.0f;
446  foreach (MapEntity linked in dockingTarget.Door.LinkedGap.linkedTo)
447  {
448  if (!(linked is Hull hull)) { continue; }
449  if (hull.Volume > largestHullSize)
450  {
451  refHull = hull;
452  largestHullSize = hull.Volume;
453  }
454  }
455  if (refHull != null)
456  {
457  return IsHorizontal ?
458  Math.Sign(refHull.WorldPosition.X - dockingTarget.Door.Item.WorldPosition.X) :
459  Math.Sign(refHull.WorldPosition.Y - dockingTarget.Door.Item.WorldPosition.Y);
460  }
461  }
462  if (dockingTarget != null)
463  {
464  int dir = IsHorizontal ?
465  Math.Sign(dockingTarget.item.WorldPosition.X - item.WorldPosition.X) :
466  Math.Sign(dockingTarget.item.WorldPosition.Y - item.WorldPosition.Y);
467  if (dir != 0) { return dir; }
468  }
469  if (item.Submarine != null)
470  {
471  return IsHorizontal ?
472  Math.Sign(item.WorldPosition.X - item.Submarine.WorldPosition.X) :
473  Math.Sign(item.WorldPosition.Y - item.Submarine.WorldPosition.Y);
474  }
475 
476  return 0;
477  }
478 
479  private int GetForcedDockingDir()
480  {
481  switch (ForceDockingDirection)
482  {
483  case DirectionType.Left:
484  return -1;
485  case DirectionType.Right:
486  return 1;
487  case DirectionType.Top:
488  return 1;
489  case DirectionType.Bottom:
490  return -1;
491  }
492  return 0;
493  }
494 
495  private void ConnectWireBetweenPorts()
496  {
497  Wire wire = item.GetComponent<Wire>();
498  if (wire == null) { return; }
499 
500  wire.Locked = true;
501  wire.Hidden = true;
502 
503  if (Item.Connections == null) { return; }
504 
505  var powerConnection = Item.Connections.Find(c => c.IsPower);
506  if (powerConnection == null) { return; }
507 
508  if (DockingTarget == null || DockingTarget.item.Connections == null) { return; }
509  var recipient = DockingTarget.item.Connections.Find(c => c.IsPower);
510  if (recipient == null) { return; }
511 
512  wire.RemoveConnection(item);
513  wire.RemoveConnection(DockingTarget.item);
514 
515  powerConnection.TryAddLink(wire);
516  wire.TryConnect(powerConnection, addNode: false);
517  recipient.TryAddLink(wire);
518  wire.TryConnect(recipient, addNode: false);
519 
520  //Flag connections to be updated
521  Powered.ChangedConnections.Add(powerConnection);
522  Powered.ChangedConnections.Add(recipient);
523  }
524 
525  private void CreateDoorBody()
526  {
527  if (doorBody != null)
528  {
529  GameMain.World.Remove(doorBody);
530  doorBody = null;
531  }
532 
533  Vector2 position = ConvertUnits.ToSimUnits(item.Position + (DockingTarget.Door.Item.WorldPosition - item.WorldPosition));
534  if (!MathUtils.IsValid(position))
535  {
536  string errorMsg =
537  "Attempted to create a door body at an invalid position (item pos: " + item.Position
538  + ", item world pos: " + item.WorldPosition
539  + ", docking target world pos: " + DockingTarget.Door.Item.WorldPosition + ")\n" + Environment.StackTrace.CleanupStackTrace();
540 
541  DebugConsole.ThrowError(errorMsg);
542  GameAnalyticsManager.AddErrorEventOnce(
543  "DockingPort.CreateDoorBody:InvalidPosition",
544  GameAnalyticsManager.ErrorSeverity.Error,
545  errorMsg);
546  position = Vector2.Zero;
547  }
548 
549  System.Diagnostics.Debug.Assert(doorBody == null);
550 
551  doorBody = GameMain.World.CreateRectangle(
554  1.0f,
555  position);
556  doorBody.UserData = DockingTarget.Door;
557  doorBody.CollisionCategories = Physics.CollisionWall;
558  doorBody.BodyType = BodyType.Static;
559  }
560 
561  private void CreateHulls()
562  {
563  var hullRects = new Rectangle[] { item.WorldRect, DockingTarget.item.WorldRect };
564  var subs = new Submarine[] { item.Submarine, DockingTarget.item.Submarine };
565 
566  bodies = new Body[4];
567  RemoveConvexHulls();
568 
569  if (DockingTarget.Door != null)
570  {
571  CreateDoorBody();
572  }
573 
574  if (Door != null)
575  {
576  DockingTarget.CreateDoorBody();
577  }
578 
579  if (IsHorizontal)
580  {
581  if (hullRects[0].Center.X > hullRects[1].Center.X)
582  {
583  hullRects = new Rectangle[] { DockingTarget.item.WorldRect, item.WorldRect };
585  }
586 
587  int scaledDockedDistance = (int)(DockedDistance / 2 * item.Scale);
588  hullRects[0] = new Rectangle(hullRects[0].Center.X, hullRects[0].Y, scaledDockedDistance, hullRects[0].Height);
589  hullRects[1] = new Rectangle(hullRects[1].Center.X - scaledDockedDistance, hullRects[1].Y, scaledDockedDistance, hullRects[1].Height);
590 
591  //expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls
592  int leftSubRightSide = int.MinValue, rightSubLeftSide = int.MaxValue;
593  foreach (Hull hull in Hull.HullList)
594  {
595  for (int i = 0; i < 2; i++)
596  {
597  if (hull.Submarine != subs[i]) { continue; }
598  if (hull.WorldRect.Y - 5 < hullRects[i].Y - hullRects[i].Height) { continue; }
599  if (hull.WorldRect.Y - hull.WorldRect.Height + 5 > hullRects[i].Y) { continue; }
600 
601  if (i == 0) //left hull
602  {
603  if (hull.WorldPosition.X > hullRects[0].Center.X) { continue; }
604  leftSubRightSide = Math.Max(hull.WorldRect.Right, leftSubRightSide);
605  }
606  else //upper hull
607  {
608  if (hull.WorldPosition.X < hullRects[1].Center.X) { continue; }
609  rightSubLeftSide = Math.Min(hull.WorldRect.X, rightSubLeftSide);
610  }
611  }
612  }
613 
614  if (leftSubRightSide == int.MinValue || rightSubLeftSide == int.MaxValue)
615  {
616  DebugConsole.NewMessage("Creating hulls between docking ports failed. Could not find a hull next to the docking port.");
617  return;
618  }
619 
620  //expand left hull to the rightmost hull of the sub at the left side
621  //(unless the difference is more than 100 units - if the distance is very large
622  //there's something wrong with the positioning of the docking ports or submarine hulls)
623  int leftHullDiff = (hullRects[0].X - leftSubRightSide) + 5;
624  if (leftHullDiff > 0)
625  {
626  if (leftHullDiff > 100)
627  {
628  DebugConsole.NewMessage("Creating hulls between docking ports failed. The leftmost docking port seems to be very far from any hulls in the left-side submarine.");
629  return;
630  }
631  else
632  {
633  hullRects[0].X -= leftHullDiff;
634  hullRects[0].Width += leftHullDiff;
635  }
636  }
637 
638  int rightHullDiff = (rightSubLeftSide - hullRects[1].Right) + 5;
639  if (rightHullDiff > 0)
640  {
641  if (rightHullDiff > 100)
642  {
643  DebugConsole.NewMessage("Creating hulls between docking ports failed. The rightmost docking port seems to be very far from any hulls in the right-side submarine.");
644  return;
645  }
646  else
647  {
648  hullRects[1].Width += rightHullDiff;
649  }
650  }
651 
652  int expand = 5;
653  for (int i = 0; i < 2; i++)
654  {
655  hullRects[i].X -= expand;
656  hullRects[i].Width += expand * 2;
657  hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition);
658  hulls[i] = new Hull(hullRects[i], subs[i])
659  {
660  RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch",
661  AvoidStaying = true,
662  IsWetRoom = true
663  };
664  hulls[i].AddToGrid(subs[i]);
665  hulls[i].FreeID();
666 
667  for (int j = 0; j < 2; j++)
668  {
669  bodies[i + j * 2] = GameMain.World.CreateEdge(
670  ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X, hullRects[i].Y - hullRects[i].Height * j)),
671  ConvertUnits.ToSimUnits(new Vector2(hullRects[i].Right, hullRects[i].Y - hullRects[i].Height * j)),
672  BodyType.Static);
673  }
674  }
675 #if CLIENT
676  for (int i = 0; i < 2; i++)
677  {
678  convexHulls[i] =
679  new ConvexHull(new Rectangle(
680  new Point((int)item.Position.X, item.Rect.Y - item.Rect.Height * i),
681  new Point((int)(DockingTarget.item.WorldPosition.X - item.WorldPosition.X), 0)), IsHorizontal, item);
682  }
683 #endif
684 
685  if (rightHullDiff <= 100 && hulls[0].Submarine != null)
686  {
687  outsideBlocker = hulls[0].Submarine.PhysicsBody.FarseerBody.CreateRectangle(
688  ConvertUnits.ToSimUnits(hullRects[0].Width + hullRects[1].Width),
689  ConvertUnits.ToSimUnits(hullRects[0].Height),
690  density: 0.0f,
691  offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Right, hullRects[0].Y - hullRects[0].Height / 2) - hulls[0].Submarine.HiddenSubPosition),
692  Physics.CollisionWall,
693  Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile);
694  outsideBlocker.UserData = this;
695  }
696 
697  gap = new Gap(new Rectangle(hullRects[0].Right - 2, hullRects[0].Y, 4, hullRects[0].Height), true, subs[0]);
698  }
699  else
700  {
701  if (hullRects[0].Center.Y > hullRects[1].Center.Y)
702  {
703  hullRects = new Rectangle[] { DockingTarget.item.WorldRect, item.WorldRect };
705  }
706 
707  int scaledDockedDistance = (int)(DockedDistance / 2 * item.Scale);
708  hullRects[0] = new Rectangle(hullRects[0].X, hullRects[0].Y - hullRects[0].Height / 2 + scaledDockedDistance, hullRects[0].Width, scaledDockedDistance);
709  hullRects[1] = new Rectangle(hullRects[1].X, hullRects[1].Y - hullRects[1].Height / 2, hullRects[1].Width, scaledDockedDistance);
710 
711  //expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls
712  int upperSubBottom = int.MaxValue, lowerSubTop = int.MinValue;
713  foreach (Hull hull in Hull.HullList)
714  {
715  for (int i = 0; i < 2; i++)
716  {
717  if (hull.Submarine != subs[i]) { continue; }
718  if (hull.WorldRect.Right - 5 < hullRects[i].X) { continue; }
719  if (hull.WorldRect.X + 5 > hullRects[i].Right) { continue; }
720 
721  if (i == 0) //lower hull
722  {
723  if (hull.WorldPosition.Y > hullRects[i].Y - hullRects[i].Height / 2) { continue; }
724  lowerSubTop = Math.Max(hull.WorldRect.Y, lowerSubTop);
725  }
726  else //upper hull
727  {
728  if (hull.WorldPosition.Y < hullRects[i].Y - hullRects[i].Height / 2) { continue; }
729  upperSubBottom = Math.Min(hull.WorldRect.Y - hull.WorldRect.Height, upperSubBottom);
730  }
731  }
732  }
733 
734  if (upperSubBottom == int.MaxValue || lowerSubTop == int.MinValue)
735  {
736  DebugConsole.NewMessage("Creating hulls between docking ports failed. Could not find a hull next to the docking port.");
737  return;
738  }
739 
740  //expand lower hull to the topmost hull of the lower sub
741  //(unless the difference is more than 100 units - if the distance is very large
742  //there's something wrong with the positioning of the docking ports or submarine hulls)
743  int lowerHullDiff = ((hullRects[0].Y - hullRects[0].Height) - lowerSubTop) + 5;
744  if (lowerHullDiff > 0)
745  {
746  if (lowerHullDiff > 100)
747  {
748  DebugConsole.NewMessage("Creating hulls between docking ports failed. The lower docking port seems to be very far from any hulls in the lower submarine.");
749  return;
750  }
751  else
752  {
753  hullRects[0].Height += lowerHullDiff;
754  }
755  }
756 
757  int upperHullDiff = (upperSubBottom - hullRects[1].Y) + 5;
758  if (upperHullDiff > 0)
759  {
760  if (upperHullDiff > 100)
761  {
762  DebugConsole.NewMessage("Creating hulls between docking ports failed. The upper docking port seems to be very far from any hulls in the upper submarine.");
763  return;
764  }
765  else
766  {
767  hullRects[1].Y += upperHullDiff;
768  hullRects[1].Height += upperHullDiff;
769  }
770  }
771 
772  //difference between the edges of the hulls (to avoid a gap between the hulls)
773  //0 is lower
774  int midHullDiff = ((hullRects[1].Y - hullRects[1].Height) - hullRects[0].Y) + 2;
775  if (midHullDiff > 100)
776  {
777  DebugConsole.NewMessage("Creating hulls between docking ports failed. The upper hull seems to be very far from the lower hull.");
778  return;
779  }
780  else if (midHullDiff > 0)
781  {
782  hullRects[0].Height += midHullDiff / 2 + 1;
783  hullRects[1].Y -= midHullDiff / 2 + 1;
784  hullRects[1].Height += midHullDiff / 2 + 1;
785  }
786 
787  int expand = 5;
788  for (int i = 0; i < 2; i++)
789  {
790  hullRects[i].Y += expand;
791  hullRects[i].Height += expand * 2;
792  hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition);
793  hulls[i] = new Hull(hullRects[i], subs[i])
794  {
795  RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch",
796  AvoidStaying = true
797  };
798  hulls[i].AddToGrid(subs[i]);
799  hulls[i].FreeID();
800 
801  for (int j = 0; j < 2; j++)
802  {
803  bodies[i + j * 2] = GameMain.World.CreateEdge(
804  ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y)),
805  ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y - hullRects[i].Height)),
806  BodyType.Static);
807  }
808  }
809 #if CLIENT
810  for (int i = 0; i < 2; i++)
811  {
812  convexHulls[i] =
813  new ConvexHull(new Rectangle(
814  new Point(item.Rect.X + item.Rect.Width * i, (int)item.Position.Y),
815  new Point(0, (int)(DockingTarget.item.WorldPosition.Y - item.WorldPosition.Y))), IsHorizontal, item);
816  }
817 #endif
818 
819  if (midHullDiff <= 100 && hulls[0].Submarine != null)
820  {
821  outsideBlocker = hulls[0].Submarine.PhysicsBody.FarseerBody.CreateRectangle(
822  ConvertUnits.ToSimUnits(hullRects[0].Width),
823  ConvertUnits.ToSimUnits(hullRects[0].Height + hullRects[1].Height),
824  density: 0.0f,
825  offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Center.X, hullRects[0].Y) - hulls[0].Submarine.HiddenSubPosition),
826  Physics.CollisionWall,
827  Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile);
828  outsideBlocker.UserData = this;
829  }
830 
831  gap = new Gap(new Rectangle(hullRects[0].X, hullRects[0].Y + 2, hullRects[0].Width, 4), false, subs[0]);
832  }
833 
834  LinkHullsToGaps();
835 
836  Item.UpdateHulls();
837 
838  hulls[0].ShouldBeSaved = false;
839  hulls[1].ShouldBeSaved = false;
840  item.linkedTo.Add(hulls[0]);
841  item.linkedTo.Add(hulls[1]);
842 
843  gap.FreeID();
844  gap.DisableHullRechecks = true;
845  gap.ShouldBeSaved = false;
846  item.linkedTo.Add(gap);
847 
848  foreach (Body body in bodies)
849  {
850  if (body == null) { continue; }
851  body.BodyType = BodyType.Static;
852  body.Friction = 0.5f;
853  }
854  }
855 
856  partial void RemoveConvexHulls();
857 
858  private void LinkHullsToGaps()
859  {
860  if (gap == null || hulls == null || hulls[0] == null || hulls[1] == null)
861  {
862 #if DEBUG
863  DebugConsole.ThrowError("Failed to link dockingport hulls to gap");
864 #endif
865  return;
866  }
867 
868  gap.linkedTo.Clear();
869 
870  if (IsHorizontal)
871  {
872  if (hulls[0].WorldRect.X > hulls[1].WorldRect.X)
873  {
874  var temp = hulls[0];
875  hulls[0] = hulls[1];
876  hulls[1] = temp;
877  }
878  gap.linkedTo.Add(hulls[0]);
879  gap.linkedTo.Add(hulls[1]);
880  }
881  else
882  {
883  if (hulls[0].WorldRect.Y < hulls[1].WorldRect.Y)
884  {
885  var temp = hulls[0];
886  hulls[0] = hulls[1];
887  hulls[1] = temp;
888  }
889  gap.linkedTo.Add(hulls[0]);
890  gap.linkedTo.Add(hulls[1]);
891  }
892 
893  for (int i = 0; i < 2; i++)
894  {
895  Gap doorGap = i == 0 ? Door?.LinkedGap : DockingTarget?.Door?.LinkedGap;
896  if (doorGap == null) { continue; }
897  doorGap.DisableHullRechecks = true;
898  if (doorGap.linkedTo.Count >= 2) { continue; }
899 
900  if (IsHorizontal)
901  {
902  if (doorGap.WorldPosition.X < gap.WorldPosition.X)
903  {
904  if (!doorGap.linkedTo.Contains(hulls[0])) { doorGap.linkedTo.Add(hulls[0]); }
905  }
906  else
907  {
908  if (!doorGap.linkedTo.Contains(hulls[1])) { doorGap.linkedTo.Add(hulls[1]); }
909  }
910  //make sure the left hull is linked to the gap first (gap logic assumes that the first hull is the one to the left)
911  if (doorGap.linkedTo.Count > 1 && doorGap.linkedTo[0].WorldRect.X > doorGap.linkedTo[1].WorldRect.X)
912  {
913  var temp = doorGap.linkedTo[0];
914  doorGap.linkedTo[0] = doorGap.linkedTo[1];
915  doorGap.linkedTo[1] = temp;
916  }
917  }
918  else
919  {
920  if (doorGap.WorldPosition.Y > gap.WorldPosition.Y)
921  {
922  if (!doorGap.linkedTo.Contains(hulls[0])) { doorGap.linkedTo.Add(hulls[0]); }
923  }
924  else
925  {
926  if (!doorGap.linkedTo.Contains(hulls[1])) { doorGap.linkedTo.Add(hulls[1]); }
927  }
928  //make sure the upper hull is linked to the gap first (gap logic assumes that the first hull is above the second one)
929  if (doorGap.linkedTo.Count > 1 && doorGap.linkedTo[0].WorldRect.Y < doorGap.linkedTo[1].WorldRect.Y)
930  {
931  var temp = doorGap.linkedTo[0];
932  doorGap.linkedTo[0] = doorGap.linkedTo[1];
933  doorGap.linkedTo[1] = temp;
934  }
935  }
936  }
937  }
938 
939  public void Undock(bool applyEffects = true)
940  {
941  if (DockingTarget == null || !docked) { return; }
942 
943  forceLockTimer = 0.0f;
944  dockingCooldown = 0.1f;
945 
946  if (applyEffects)
947  {
948  ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f);
949  }
950 
955 
956  if (Door != null && DockingTarget.Door != null)
957  {
958  WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => Door.LinkedGap == wp.ConnectedGap);
959  WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.Door.LinkedGap == wp.ConnectedGap);
960 
961  if (myWayPoint != null && targetWayPoint != null)
962  {
963  myWayPoint.FindHull();
964  if (myWayPoint.linkedTo.Contains(targetWayPoint))
965  {
966  myWayPoint.linkedTo.Remove(targetWayPoint);
967  myWayPoint.OnLinksChanged?.Invoke(myWayPoint);
968  }
969  targetWayPoint.FindHull();
970  if (targetWayPoint.linkedTo.Contains(myWayPoint))
971  {
972  targetWayPoint.linkedTo.Remove(myWayPoint);
973  targetWayPoint.OnLinksChanged?.Invoke(targetWayPoint);
974  }
975  }
976  }
977 
978  item.linkedTo.Clear();
979 
980  docked = false;
981  item.SendSignal("1", "on_undock");
982 
985  obstructedWayPointsDisabled = false;
986 
988  DockingTarget = null;
989 
990  //Flag power connection
991  Connection powerConnection = Item.Connections.Find(c => c.IsPower);
992  if (powerConnection != null)
993  {
994  Powered.ChangedConnections.Add(powerConnection);
995  }
996 
997  if (doorBody != null)
998  {
999  GameMain.World.Remove(doorBody);
1000  doorBody = null;
1001  }
1002 
1003  var wire = item.GetComponent<Wire>();
1004  wire?.Drop(null);
1005 
1006  if (joint != null)
1007  {
1008  GameMain.World.Remove(joint);
1009  joint = null;
1010  }
1011 
1012  hulls[0]?.Remove(); hulls[0] = null;
1013  hulls[1]?.Remove(); hulls[1] = null;
1014 
1015  RemoveConvexHulls();
1016 
1017  if (gap != null)
1018  {
1019  gap.Remove();
1020  gap = null;
1021  }
1022 
1023  if (bodies != null)
1024  {
1025  foreach (Body body in bodies)
1026  {
1027  if (body == null) { continue; }
1028  GameMain.World.Remove(body);
1029  }
1030  bodies = null;
1031  }
1032 
1033  outsideBlocker?.Body.Remove(outsideBlocker);
1034  outsideBlocker = null;
1035 
1036 #if SERVER
1037  if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
1038  {
1039  item.CreateServerEvent(this);
1040  }
1041 #elif CLIENT
1042  autodockingVerification?.Close();
1043  autodockingVerification = null;
1044 #endif
1045  OnUnDocked?.Invoke();
1046  OnUnDocked = null;
1047  }
1048 
1049  public override void Update(float deltaTime, Camera cam)
1050  {
1051  dockingCooldown -= deltaTime;
1052  if (DockingTarget == null)
1053  {
1054  dockingState = MathHelper.Lerp(dockingState, 0.0f, deltaTime * 10.0f);
1055  if (dockingState < 0.01f) { docked = false; }
1056  item.SendSignal("0", "state_out");
1057  item.SendSignal(AnotherPortInProximity ? "1" : "0", "proximity_sensor");
1058  }
1059  else
1060  {
1061  if (!docked)
1062  {
1064  if (DockingTarget == null) { return; }
1065  }
1066 
1067  if (joint is DistanceJoint)
1068  {
1069  dockingState = MathHelper.Lerp(dockingState, 0.5f, deltaTime * 10.0f);
1070 
1071  forceLockTimer += deltaTime;
1072 
1073  Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
1074 
1075  if (jointDiff.LengthSquared() > 0.04f * 0.04f && forceLockTimer < ForceLockDelay)
1076  {
1078  float massRatio1, massRatio2;
1079  if (item.Submarine.PhysicsBody.BodyType != BodyType.Dynamic)
1080  {
1081  massRatio1 = 0.0f;
1082  massRatio2 = 1.0f;
1083  }
1084  else if (DockingTarget.item.Submarine.PhysicsBody.BodyType != BodyType.Dynamic)
1085  {
1086  massRatio1 = 1.0f;
1087  massRatio2 = 0.0f;
1088  }
1089  else
1090  {
1091  massRatio1 = DockingTarget.item.Submarine.PhysicsBody.Mass / totalMass;
1092  massRatio2 = item.Submarine.PhysicsBody.Mass / totalMass;
1093  }
1094 
1095  Vector2 relativeVelocity = DockingTarget.item.Submarine.Velocity - item.Submarine.Velocity;
1096  Vector2 desiredRelativeVelocity = Vector2.Normalize(jointDiff);
1097 
1098  item.Submarine.Velocity += (relativeVelocity + desiredRelativeVelocity) * massRatio1;
1099  DockingTarget.item.Submarine.Velocity += (-relativeVelocity - desiredRelativeVelocity) * massRatio2;
1100  }
1101  else
1102  {
1103  Lock(isNetworkMessage: false);
1104  }
1105  }
1106  else
1107  {
1108  if (DockingTarget.Door != null && doorBody != null)
1109  {
1110  doorBody.Enabled = DockingTarget.Door.Body.Enabled && !(DockingTarget.Door.Body.FarseerBody.FixtureList.FirstOrDefault()?.IsSensor ?? false);
1111  }
1112  dockingState = MathHelper.Lerp(dockingState, 1.0f, deltaTime * 10.0f);
1113  }
1114 
1115  item.SendSignal(IsLocked ? "1" : "0", "state_out");
1116  }
1117  if (!obstructedWayPointsDisabled && dockingState >= 0.99f)
1118  {
1121  obstructedWayPointsDisabled = true;
1122  }
1123  }
1124 
1125  protected override void RemoveComponentSpecific()
1126  {
1127  base.RemoveComponentSpecific();
1128  list.Remove(this);
1129  hulls[0]?.Remove(); hulls[0] = null;
1130  hulls[1]?.Remove(); hulls[1] = null;
1131  gap?.Remove(); gap = null;
1132  RemoveConvexHulls();
1133 
1134  overlaySprite?.Remove();
1135  overlaySprite = null;
1136  }
1137 
1138  private bool initialized = false;
1139  private void InitializeLinks()
1140  {
1141  if (initialized) { return; }
1142  initialized = true;
1143 
1144  float maxXDist = (item.Prefab.Sprite.size.X * item.Prefab.Scale) / 2;
1145  float closestYDist = (item.Prefab.Sprite.size.Y * item.Prefab.Scale) / 2;
1146  foreach (Item it in Item.ItemList)
1147  {
1148  if (it.Submarine != item.Submarine) { continue; }
1149 
1150  var doorComponent = it.GetComponent<Door>();
1151  if (doorComponent == null || doorComponent.IsHorizontal == IsHorizontal) { continue; }
1152 
1153  float yDist = Math.Abs(it.Position.Y - item.Position.Y);
1154  if (item.linkedTo.Contains(it))
1155  {
1156  // If there's a door linked to the docking port, always treat it close enough.
1157  yDist = Math.Min(closestYDist, yDist);
1158  }
1159  else if (Math.Abs(it.Position.X - item.Position.X) > maxXDist)
1160  {
1161  // Too far left/right
1162  continue;
1163  }
1164 
1165  if (yDist <= closestYDist)
1166  {
1167  Door = doorComponent;
1168  closestYDist = yDist;
1169  }
1170  }
1171 
1172  if (!item.linkedTo.Any()) { return; }
1173 
1174  List<MapEntity> linked = new List<MapEntity>(item.linkedTo);
1175  foreach (MapEntity entity in linked)
1176  {
1177  if (entity is Hull hull)
1178  {
1179  hull.Remove();
1180  item.linkedTo.Remove(hull);
1181  continue;
1182  }
1183 
1184  if (entity is Gap gap)
1185  {
1186  gap.Remove();
1187  continue;
1188  }
1189  }
1190  }
1191 
1192  public override void OnMapLoaded()
1193  {
1194  InitializeLinks();
1195 
1196  Wire wire = item.GetComponent<Wire>();
1197  if (wire != null)
1198  {
1199  wire.Locked = true;
1200  wire.Hidden = true;
1201  if (wire.Connections.Contains(null))
1202  {
1203  wire.Drop(null);
1204  }
1205  }
1206 
1207  if (!item.linkedTo.Any()) { return; }
1208 
1209  List<MapEntity> linked = new List<MapEntity>(item.linkedTo);
1210  foreach (MapEntity entity in linked)
1211  {
1212  if (!(entity is Item linkedItem)) { continue; }
1213 
1214  var dockingPort = linkedItem.GetComponent<DockingPort>();
1215  if (dockingPort != null)
1216  {
1217  Dock(dockingPort);
1218  }
1219  }
1220  }
1221 
1222  public override void ReceiveSignal(Signal signal, Connection connection)
1223  {
1224 #if CLIENT
1225  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
1226  {
1227  return;
1228  }
1230  {
1231  return;
1232  }
1233 #endif
1234 
1235  if (dockingCooldown > 0.0f) { return; }
1236 
1237  bool wasDocked = docked;
1238  DockingPort prevDockingTarget = DockingTarget;
1239 
1240  bool newDockedState = wasDocked;
1241  switch (connection.Name)
1242  {
1243  case "toggle":
1244  if (signal.value != "0")
1245  {
1246  newDockedState = !docked;
1247  }
1248  break;
1249  case "set_active":
1250  case "set_state":
1251  newDockedState = signal.value != "0";
1252  break;
1253  }
1254 
1255  if (newDockedState != wasDocked)
1256  {
1257  bool tryingToToggleOutpostDocking = docked ?
1258  DockingTarget?.Item?.Submarine?.Info?.IsOutpost ?? false :
1259  FindAdjacentPort()?.Item?.Submarine?.Info?.IsOutpost ?? false;
1260  //trying to dock/undock from an outpost and the signal was sent by some automated system instead of a character
1261  // -> ask if the player really wants to dock/undock to prevent a softlock if someone's wired the docking port
1262  // in a way that makes always makes it dock/undock immediately at the start of the roun
1263  if (GameMain.NetworkMember != null && tryingToToggleOutpostDocking && signal.sender == null)
1264  {
1265  if (allowOutpostAutoDocking == AllowOutpostAutoDocking.Ask)
1266  {
1267 #if CLIENT
1268  if (!outpostAutoDockingPromptShown)
1269  {
1270  autodockingVerification = new GUIMessageBox(string.Empty,
1271  TextManager.Get(newDockedState ? "autodockverification" : "autoundockverification"),
1272  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });
1273  autodockingVerification.Buttons[0].OnClicked += (btn, userdata) =>
1274  {
1275  autodockingVerification?.Close();
1276  autodockingVerification = null;
1277  if (item.Removed || GameMain.Client == null) { return false; }
1278  allowOutpostAutoDocking = AllowOutpostAutoDocking.Yes;
1279  item.CreateClientEvent(this);
1280  return true;
1281  };
1282  autodockingVerification.Buttons[1].OnClicked += (btn, userdata) =>
1283  {
1284  autodockingVerification?.Close();
1285  autodockingVerification = null;
1286  if (item.Removed || GameMain.Client == null) { return false; }
1287  allowOutpostAutoDocking = AllowOutpostAutoDocking.No;
1288  item.CreateClientEvent(this);
1289  return true;
1290  };
1291  }
1292 #endif
1293  outpostAutoDockingPromptShown = true;
1294  return;
1295  }
1296  else if (allowOutpostAutoDocking == AllowOutpostAutoDocking.No)
1297  {
1298  return;
1299  }
1300  }
1301 
1302  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
1303 
1304  Docked = newDockedState;
1305  }
1306 
1307 #if SERVER
1308  if (signal.sender != null && docked != wasDocked)
1309  {
1310  if (docked)
1311  {
1312  if (item.Submarine != null && DockingTarget?.item?.Submarine != null)
1313  GameServer.Log(GameServer.CharacterLogName(signal.sender) + " docked " + item.Submarine.Info.Name + " to " + DockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction);
1314  }
1315  else
1316  {
1317  if (item.Submarine != null && prevDockingTarget?.item?.Submarine != null)
1318  GameServer.Log(GameServer.CharacterLogName(signal.sender) + " undocked " + item.Submarine.Info.Name + " from " + prevDockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction);
1319  }
1320  }
1321 #endif
1322  }
1323  }
1324 }
Vector2 Position
Definition: Camera.cs:398
static bool AllowedToManageCampaign(ClientPermissions permissions)
There is a server-side implementation of the method in MultiPlayerCampaign
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
void FreeID()
Removes the entity from the entity dictionary and frees up the ID it was using.
Definition: Entity.cs:302
List< GUIButton > Buttons
static GameSession?? GameSession
Definition: GameMain.cs:88
static World World
Definition: GameMain.cs:105
static GameScreen GameScreen
Definition: GameMain.cs:52
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static GameClient Client
Definition: GameMain.cs:188
static void UpdateHulls()
goes through every item and re-checks which hull they are in
static readonly List< Item > ItemList
void SendSignal(string signal, string connectionName)
override void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
Action OnUnDocked
Automatically cleared after undocking -> no need to unregister
Action OnDocked
Automatically cleared after docking -> no need to unregister
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)
override void Drop(Character dropper, bool setTransform=true)
a Character has dropped the item
ContentPath FilePath
Definition: Prefab.cs:38
readonly Dictionary< Submarine, DockingPort > ConnectedDockingPorts
void EnableObstructedWaypoints(Submarine otherSub)
Only affects temporarily disabled waypoints.
void DisableObstructedWayPoints()
Permanently disables obstructed waypoints obstructed by the level.
Interface for entities that the server can send events to the clients
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19