Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs
2 using FarseerPhysics;
3 using FarseerPhysics.Dynamics;
4 using FarseerPhysics.Dynamics.Contacts;
5 using FarseerPhysics.Dynamics.Joints;
6 using Microsoft.Xna.Framework;
7 using System;
8 using System.Collections.Generic;
9 using System.Linq;
10 using System.Xml.Linq;
12 using LimbParams = Barotrauma.RagdollParams.LimbParams;
13 using JointParams = Barotrauma.RagdollParams.JointParams;
14 using MoonSharp.Interpreter;
15 
16 namespace Barotrauma
17 {
18  abstract partial class Ragdoll
19  {
20  public abstract RagdollParams RagdollParams { get; protected set; }
21 
22  const float ImpactDamageMultiplayer = 10.0f;
26  const float MaxImpactDamage = 0.1f;
27 
28  private static readonly List<Ragdoll> list = new List<Ragdoll>();
29 
30  struct Impact
31  {
32  public Fixture F1, F2;
33  public Vector2 LocalNormal;
34  public Vector2 Velocity;
35  public Vector2 ImpactPos;
36 
37  public Impact(Fixture f1, Fixture f2, Contact contact, Vector2 velocity)
38  {
39  F1 = f1;
40  F2 = f2;
41  Velocity = velocity;
42  LocalNormal = contact.Manifold.LocalNormal;
43  contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2<Vector2> points);
44  ImpactPos = points[0];
45  }
46  }
47 
48  private readonly Queue<Impact> impactQueue = new Queue<Impact>();
49 
50  protected Hull currentHull;
51 
52  private bool accessRemovedCharacterErrorShown;
53 
54  private Limb[] limbs;
55  public Limb[] Limbs
56  {
57  get
58  {
59  if (limbs == null)
60  {
62  return Array.Empty<Limb>();
63  }
64  return limbs;
65  }
66  }
67 
68  public bool HasMultipleLimbsOfSameType => limbs != null && limbs.Length > limbDictionary.Count;
69 
70  private bool frozen;
71  public bool Frozen
72  {
73  get { return frozen; }
74  set
75  {
76  if (frozen == value) return;
77 
78  frozen = value;
79 
80  Collider.FarseerBody.LinearDamping = frozen ? (1.5f / (float)Timing.Step) : 0.0f;
81  Collider.FarseerBody.AngularDamping = frozen ? (1.5f / (float)Timing.Step) : PhysicsBody.DefaultAngularDamping;
82  Collider.FarseerBody.IgnoreGravity = frozen;
83 
84  //Collider.PhysEnabled = !frozen;
85  if (frozen && MainLimb != null) { MainLimb.PullJointWorldAnchorB = MainLimb.SimPosition; }
86  }
87  }
88 
89  private Dictionary<LimbType, Limb> limbDictionary;
91 
92  private bool simplePhysicsEnabled;
93 
95  protected Character character;
96 
97  protected float strongestImpact;
98 
99  private float splashSoundTimer;
100 
101  //the ragdoll builds a "tolerance" to the flow force when being pushed by water.
102  //Allows sudden forces (breach, letting water through a door) to heavily push the character around while ensuring flowing water won't make the characters permanently stuck.
103  private float flowForceTolerance, flowStunTolerance;
104 
105  //the movement speed of the ragdoll
106  public Vector2 movement;
107  //the target speed towards which movement is interpolated
108  protected Vector2 targetMovement;
109 
110  //a movement vector that overrides targetmovement if trying to steer
111  //a Character to the position sent by server in multiplayer mode
112  protected Vector2 overrideTargetMovement;
113 
114  protected float floorY, standOnFloorY;
115  protected Fixture floorFixture;
116  protected Vector2 floorNormal = Vector2.UnitY;
117  protected float surfaceY;
118 
119  protected bool inWater, headInWater;
120  protected bool onGround;
121  public bool OnGround => onGround;
122  private Vector2 lastFloorCheckPos;
123  private bool lastFloorCheckIgnoreStairs, lastFloorCheckIgnorePlatforms;
124 
125 
130 
132 
133  protected Direction dir;
134 
136 
137  protected List<PhysicsBody> collider;
138  protected int colliderIndex = 0;
139 
140  private Category prevCollisionCategory = Category.None;
141 
142  public bool IsStuck => Limbs.Any(l => l.IsStuck);
143 
145  {
146  get
147  {
148  return collider?[colliderIndex];
149  }
150  }
151 
152  public bool TryGetCollider(int index, out PhysicsBody collider)
153  {
154  collider = null;
155  try
156  {
157  collider = this.collider?[index];
158  return true;
159  }
160  catch
161  {
162  return false;
163  }
164  }
165 
166  public int ColliderIndex
167  {
168  get
169  {
170  return colliderIndex;
171  }
172  set
173  {
174  if (value == colliderIndex || collider == null) { return; }
175  if (value >= collider.Count || value < 0) { return; }
176 
177  if (collider[colliderIndex].Height < collider[value].Height)
178  {
179  Vector2 pos1 = collider[colliderIndex].SimPosition;
180  pos1.Y -= collider[colliderIndex].Height * ColliderHeightFromFloor;
181  Vector2 pos2 = pos1;
182  pos2.Y += collider[value].Height * 1.1f;
183  if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall) && !(f.Body.UserData is Submarine))) { return; }
184  }
185 
186  Vector2 pos = collider[colliderIndex].SimPosition;
187  pos.Y -= collider[colliderIndex].Height * 0.5f;
188  pos.Y += collider[value].Height * 0.5f;
189  collider[value].SetTransform(pos, collider[colliderIndex].Rotation);
190 
191  collider[value].LinearVelocity = collider[colliderIndex].LinearVelocity;
192  collider[value].AngularVelocity = collider[colliderIndex].AngularVelocity;
193  collider[value].Submarine = collider[colliderIndex].Submarine;
194  collider[value].PhysEnabled = !frozen;
195  collider[value].Enabled = !simplePhysicsEnabled;
196 
197  collider[colliderIndex].PhysEnabled = false;
198  colliderIndex = value;
199  }
200  }
201 
202  public float FloorY
203  {
204  get { return floorY; }
205  }
206 
207  public float Mass
208  {
209  get;
210  private set;
211  }
212 
213  public void SubtractMass(Limb limb)
214  {
215  if (limbs.Contains(limb))
216  {
217  Mass -= limb.Mass;
218  }
219  }
220 
221  public Limb MainLimb
222  {
223  get
224  {
225  Limb mainLimb = GetLimb(RagdollParams.MainLimb);
226  if (!IsValid(mainLimb))
227  {
228  Limb torso = GetLimb(LimbType.Torso);
229  Limb head = GetLimb(LimbType.Head);
230  mainLimb = torso ?? head;
231  if (!IsValid(mainLimb))
232  {
233  mainLimb = Limbs.FirstOrDefault(l => IsValid(l));
234  }
235  if (mainLimb == null)
236  {
237  DebugConsole.ThrowError("Couldn't find a valid main limb. The limb can't be hidden nor be set to ignore collisions!");
238  mainLimb = Limbs.FirstOrDefault();
239  }
240  }
241  static bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.IgnoreCollisions && !limb.Hidden;
242  return mainLimb;
243  }
244  }
245 
246  public Vector2 WorldPosition
247  {
248  get
249  {
250  return character.Submarine == null ?
251  ConvertUnits.ToDisplayUnits(Collider.SimPosition) :
252  ConvertUnits.ToDisplayUnits(Collider.SimPosition) + character.Submarine.Position;
253  }
254  }
255 
257  {
258  get { return simplePhysicsEnabled; }
259  set
260  {
261  if (value == simplePhysicsEnabled) { return; }
262 
263  simplePhysicsEnabled = value;
264 
265  foreach (Limb limb in Limbs)
266  {
267  if (limb.IsSevered) { continue; }
268  if (limb.body == null)
269  {
270  DebugConsole.ThrowError("Limb has no body! (" + (character != null ? character.Name : "Unknown character") + ", " + limb.type.ToString());
271  continue;
272  }
273  limb.body.Enabled = !simplePhysicsEnabled;
274  }
275 
276  foreach (LimbJoint joint in LimbJoints)
277  {
278  joint.Enabled = !joint.IsSevered && !simplePhysicsEnabled;
279  }
280 
281  if (!simplePhysicsEnabled)
282  {
283  foreach (Limb limb in Limbs)
284  {
285  if (limb.IsSevered || !limb.body.PhysEnabled) { continue; }
287  //reset pull joints (they may be somewhere far away if the character has moved from the position where animations were last updated)
288  limb.PullJointEnabled = false;
290  }
291  }
292  }
293  }
294 
295  public const float MAX_SPEED = 20;
296 
297  public Vector2 TargetMovement
298  {
299  get
300  {
301  return (overrideTargetMovement == Vector2.Zero) ? targetMovement : overrideTargetMovement;
302  }
303  set
304  {
305  if (!MathUtils.IsValid(value)) return;
306  targetMovement.X = MathHelper.Clamp(value.X, -MAX_SPEED, MAX_SPEED);
307  targetMovement.Y = MathHelper.Clamp(value.Y, -MAX_SPEED, MAX_SPEED);
308  }
309  }
310 
311  public abstract float? HeadPosition { get; }
312  public abstract float? HeadAngle { get; }
313  public abstract float? TorsoPosition { get; }
314  public abstract float? TorsoAngle { get; }
315 
316  float? impactTolerance;
317  public float ImpactTolerance
318  {
319  get
320  {
321  if (impactTolerance == null)
322  {
323  impactTolerance = RagdollParams.ImpactTolerance;
324  if (character.Params.VariantFile != null)
325  {
326  float? tolerance = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("ragdoll")?.GetAttributeFloat("impacttolerance", impactTolerance.Value);
327  if (tolerance.HasValue)
328  {
329  impactTolerance = tolerance;
330  }
331  }
332  }
333  return impactTolerance.Value;
334  }
335  }
336 
338 
340 
341  public float Dir => dir == Direction.Left ? -1.0f : 1.0f;
342 
344 
345  public bool InWater
346  {
347  get { return inWater; }
348  }
349 
350  public bool HeadInWater
351  {
352  get { return headInWater; }
353  }
354 
356  {
357  get { return currentHull; }
358  set
359  {
360  if (value == currentHull) return;
361 
362  currentHull = value;
363  Submarine currSubmarine = currentHull?.Submarine;
364  foreach (Limb limb in Limbs)
365  {
366  if (limb.IsSevered) { continue; }
367  limb.body.Submarine = currSubmarine;
368  }
369  Collider.Submarine = currSubmarine;
370  }
371  }
372 
373  public bool IgnorePlatforms { get; set; }
374 
378  public virtual void Recreate(RagdollParams ragdollParams = null)
379  {
380  if (IsFlipped)
381  {
382  Flip();
383  }
384  dir = Direction.Right;
385  Dictionary<LimbParams, List<WearableSprite>> items = null;
386  if (ragdollParams != null)
387  {
388  RagdollParams = ragdollParams;
389  if (!character.VariantOf.IsEmpty)
390  {
392  }
393  }
394  else
395  {
396  items = limbs?.ToDictionary(l => l.Params, l => l.WearingItems);
397  }
398  foreach (var limbParams in RagdollParams.Limbs)
399  {
400  if (!PhysicsBody.IsValidShape(limbParams.Radius, limbParams.Height, limbParams.Width))
401  {
402  DebugConsole.ThrowError($"Invalid collider dimensions (r: {limbParams.Radius}, h: {limbParams.Height}, w: {limbParams.Width}) on limb: {limbParams.Name}. Fixing.");
403  limbParams.Radius = 10;
404  }
405  }
406  foreach (var colliderParams in RagdollParams.Colliders)
407  {
408  if (!PhysicsBody.IsValidShape(colliderParams.Radius, colliderParams.Height, colliderParams.Width))
409  {
410  DebugConsole.ThrowError($"Invalid collider dimensions (r: {colliderParams.Radius}, h: {colliderParams.Height}, w: {colliderParams.Width}) on collider: {colliderParams.Name}. Fixing.");
411  colliderParams.Radius = 10;
412  }
413  }
414  CreateColliders();
415  CreateLimbs();
416  CreateJoints();
417  UpdateCollisionCategories();
419  if (items != null)
420  {
421  foreach (var kvp in items)
422  {
423  int id = kvp.Key.ID;
424  // This can be the case if we manipulate the ragdoll at runtime (husk appendage, limb removal in the character editor)
425  if (id > limbs.Length - 1) { continue; }
426  var limb = limbs[id];
427  var itemList = kvp.Value;
428  limb.WearingItems.AddRange(itemList);
429  }
430  }
431 
433  {
434  bool inEditor = false;
435 #if CLIENT
437 #endif
438 
439  var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath);
440  if (characterPrefab?.ConfigElement != null)
441  {
442  var mainElement = characterPrefab.ConfigElement;
443  foreach (var huskAppendage in mainElement.GetChildElements("huskappendage"))
444  {
445  if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; }
446 
447  Identifier afflictionIdentifier = huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty);
448  if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab affliction) ||
449  affliction is not AfflictionPrefabHusk matchingAffliction)
450  {
451  DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!",
452  contentPackage: huskAppendage.ContentPackage);
453  }
454  else
455  {
456  AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskAppendage, ragdoll: this);
457  }
458  }
459  }
460  }
461  }
462 
463  public Ragdoll(Character character, string seed, RagdollParams ragdollParams = null)
464  {
465  list.Add(this);
466  this.character = character;
467  Recreate(ragdollParams ?? RagdollParams);
468  }
469 
470  protected void CreateColliders()
471  {
472  collider?.ForEach(c => c.Remove());
473  DebugConsole.Log($"Creating colliders from {RagdollParams.Name}.");
474  collider = new List<PhysicsBody>();
475  foreach (var cParams in RagdollParams.Colliders)
476  {
477  if (!PhysicsBody.IsValidShape(cParams.Radius, cParams.Height, cParams.Width))
478  {
479  DebugConsole.ThrowError("Invalid collider dimensions: " + cParams.Name);
480  break; ;
481  }
482  var body = new PhysicsBody(cParams);
483  collider.Add(body);
484  body.UserData = character;
485  body.FarseerBody.OnCollision += OnLimbCollision;
486  if (collider.Count > 1)
487  {
488  body.PhysEnabled = false;
489  }
490  }
491  }
492 
493  protected void CreateJoints()
494  {
495  if (LimbJoints != null)
496  {
497  foreach (LimbJoint joint in LimbJoints)
498  {
499  if (GameMain.World.JointList.Contains(joint.Joint)) { GameMain.World.Remove(joint.Joint); }
500  }
501  }
502  DebugConsole.Log($"Creating joints from {RagdollParams.Name}.");
504  RagdollParams.Joints.ForEach(j => AddJoint(j));
505  // Check the joints
506  for (int i = 0; i < LimbJoints.Length; i++)
507  {
508  if (LimbJoints[i] == null)
509  {
510  DebugConsole.ThrowError($"Joint {i} null.");
511  }
512  }
513 
514  UpdateCollisionCategories();
515  SetInitialLimbPositions();
516  }
517 
518  private void SetInitialLimbPositions()
519  {
520  foreach (var joint in LimbJoints)
521  {
522  if (joint == null) { continue; }
523  float angle = (joint.LowerLimit + joint.UpperLimit) / 2.0f;
524  joint.LimbB?.body?.SetTransform(
525  (joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, joint.BodyA.Rotation + angle, true)),
526  joint.BodyA.Rotation + angle);
527  }
528  }
529 
530  protected void CreateLimbs()
531  {
532  limbs?.ForEach(l => l.Remove());
533  DebugConsole.Log($"Creating limbs from {RagdollParams.Name}.");
534  limbDictionary = new Dictionary<LimbType, Limb>();
535  limbs = new Limb[RagdollParams.Limbs.Count];
536  RagdollParams.Limbs.ForEach(l => AddLimb(l));
537  if (limbs.Contains(null)) { return; }
538  SetupDrawOrder();
539  }
540 
541  partial void SetupDrawOrder();
542 
546  public void SaveRagdoll(string fileNameWithoutExtension = null)
547  {
548  RagdollParams.Save(fileNameWithoutExtension);
549  }
550 
555  public void ResetRagdoll(bool forceReload = false)
556  {
557  RagdollParams.Reset(forceReload);
558  ResetJoints();
559  ResetLimbs();
560  }
561 
565  public void ResetJoints()
566  {
567  LimbJoints.ForEach(j => j.LoadParams());
568  }
569 
573  public void ResetLimbs()
574  {
575  Limbs.ForEach(l => l.LoadParams());
576  SetupDrawOrder();
577  }
578 
579  public void AddJoint(JointParams jointParams)
580  {
581  if (!checkLimbIndex(jointParams.Limb2, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2"))
582  {
583  return;
584  }
585  LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this);
586  GameMain.World.Add(joint.Joint);
587  for (int i = 0; i < LimbJoints.Length; i++)
588  {
589  if (LimbJoints[i] != null) continue;
590  LimbJoints[i] = joint;
591  return;
592  }
593  Array.Resize(ref LimbJoints, LimbJoints.Length + 1);
594  LimbJoints[LimbJoints.Length - 1] = joint;
595 
596  bool checkLimbIndex(int index, string debugName)
597  {
598  if (index < 0 || index >= limbs.Length)
599  {
600  string errorMsg = $"Failed to add a joint to character {character.Name}. {debugName} out of bounds (index: {index}, limbs: {limbs.Length}.";
601  DebugConsole.ThrowError(errorMsg, contentPackage: jointParams.Element?.ContentPackage);
602  if (jointParams.Element?.ContentPackage == GameMain.VanillaContent)
603  {
604  GameAnalyticsManager.AddErrorEventOnce("Ragdoll.AddJoint:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
605  }
606  return false;
607  }
608  return true;
609  }
610  }
611 
612  protected void AddLimb(LimbParams limbParams)
613  {
614  if (limbParams.ID < 0 || limbParams.ID > 255)
615  {
616  throw new Exception($"Invalid limb params in limb \"{limbParams.Type}\". \"{limbParams.ID}\" is not a valid limb ID.");
617  }
618  byte ID = Convert.ToByte(limbParams.ID);
619  Limb limb = new Limb(this, character, limbParams);
620  limb.body.FarseerBody.OnCollision += OnLimbCollision;
621  if (ID >= Limbs.Length)
622  {
623  throw new Exception($"Failed to add a limb to the character \"{Character?.ConfigPath ?? "null"}\" (limb index {ID} out of bounds). The ragdoll file may be configured incorrectly.");
624  }
625  Limbs[ID] = limb;
626  Mass += limb.Mass;
627  if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); }
628  }
629 
630  public void AddLimb(Limb limb)
631  {
632  if (Limbs.Contains(limb)) { return; }
633  limb.body.FarseerBody.OnCollision += OnLimbCollision;
634  Array.Resize(ref limbs, Limbs.Length + 1);
635  Limbs[Limbs.Length - 1] = limb;
636  Mass += limb.Mass;
637  if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); }
638  SetupDrawOrder();
639  }
640 
641  public void RemoveLimb(Limb limb)
642  {
643  if (!Limbs.Contains(limb)) return;
644 
645  Limb[] newLimbs = new Limb[Limbs.Length - 1];
646 
647  int i = 0;
648  foreach (Limb existingLimb in Limbs)
649  {
650  if (existingLimb == limb) continue;
651  newLimbs[i] = existingLimb;
652  i++;
653  }
654 
655  limbs = newLimbs;
656  if (limbDictionary.ContainsKey(limb.type))
657  {
658  limbDictionary.Remove(limb.type);
659  // If there is another limb of the same type, replace the limb in the dictionary.
661  {
662  var otherLimb = Limbs.FirstOrDefault(l => l != limb && l.type == limb.type);
663  if (otherLimb != null)
664  {
665  limbDictionary.Add(otherLimb.type, otherLimb);
666  }
667  }
668  }
669 
670  // TODO: this could be optimized if needed, but at least we need to remove the limb from the inversedDrawOrder array.
671  SetupDrawOrder();
672 
673  //remove all joints that were attached to the removed limb
674  LimbJoint[] attachedJoints = Array.FindAll(LimbJoints, lj => lj.LimbA == limb || lj.LimbB == limb);
675  if (attachedJoints.Length > 0)
676  {
677  LimbJoint[] newJoints = new LimbJoint[LimbJoints.Length - attachedJoints.Length];
678  i = 0;
679  foreach (LimbJoint limbJoint in LimbJoints)
680  {
681  if (attachedJoints.Contains(limbJoint)) continue;
682  newJoints[i] = limbJoint;
683  i++;
684  }
685  LimbJoints = newJoints;
686  }
687 
688  SubtractMass(limb);
689  limb.Remove();
690  foreach (LimbJoint limbJoint in attachedJoints)
691  {
692  GameMain.World.Remove(limbJoint.Joint);
693  }
694  }
695 
696  private enum LimbStairCollisionResponse
697  {
698  DontClimbStairs,
699  ClimbWithoutLimbCollision,
700  ClimbWithLimbCollision
701  }
702 
703  public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact)
704  {
705  if (f2.Body.UserData is Submarine submarine && character.Submarine == submarine) { return false; }
706  if (f2.UserData is Hull)
707  {
708  if (character.Submarine != null)
709  {
710  return false;
711  }
712  if (CanEnterSubmarine == CanEnterSubmarine.Partial)
713  {
714  //collider collides with hulls to prevent the character going fully inside the sub, limbs don't
715  return
716  f1.Body == Collider.FarseerBody ||
717  (f1.Body.UserData is Limb limb && !limb.Params.CanEnterSubmarine);
718  }
719  }
720 
721  //using the velocity of the limb would make the impact damage more realistic,
722  //but would also make it harder to edit the animations because the forces/torques
723  //would all have to be balanced in a way that prevents the character from doing
724  //impact damage to itself
725  Vector2 velocity = Collider.LinearVelocity;
726  if (character.Submarine == null && f2.Body.UserData is Submarine sub)
727  {
728  velocity -= sub.Velocity;
729  }
730 
731  //always collides with bodies other than structures
732  if (f2.Body.UserData is not Structure structure)
733  {
734  if (!f2.IsSensor)
735  {
736  lock (impactQueue)
737  {
738  impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
739  }
740  }
741  return true;
742  }
743  else if (character.Submarine != null && structure.Submarine != null && character.Submarine != structure.Submarine)
744  {
745  return false;
746  }
747 
748  Vector2 colliderBottom = GetColliderBottom();
749  if (structure.IsPlatform)
750  {
751  if (IgnorePlatforms || currentHull == null) { return false; }
752 
753  if (colliderBottom.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; }
754  if (f1.Body.Position.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; }
755  }
756  else if (structure.StairDirection != Direction.None)
757  {
758  if (character.SelectedBy != null)
759  {
761  }
762 
763  var collisionResponse = getStairCollisionResponse();
764  if (collisionResponse == LimbStairCollisionResponse.ClimbWithLimbCollision)
765  {
766  Stairs = structure;
767  }
768  else
769  {
770  if (collisionResponse == LimbStairCollisionResponse.DontClimbStairs) { Stairs = null; }
771 
772  return false;
773  }
774 
775  LimbStairCollisionResponse getStairCollisionResponse()
776  {
777  //don't collide with stairs if
778 
779  //1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards
780  float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10);
781  if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; }
782  if (character.SelectedBy != null &&
783  character.SelectedBy.AnimController.GetColliderBottom().Y < stairBottomPos &&
785  {
786  return LimbStairCollisionResponse.DontClimbStairs;
787  }
788 
789  //2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards
790  if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return LimbStairCollisionResponse.DontClimbStairs; }
791 
792  //3. collided with the stairs from below
793  if (contact.Manifold.LocalNormal.Y < 0.0f)
794  {
795  return Stairs != structure
796  ? LimbStairCollisionResponse.DontClimbStairs
797  : LimbStairCollisionResponse.ClimbWithoutLimbCollision;
798  }
799 
800  //4. contact points is above the bottom half of the collider
801  contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2<Vector2> points);
802  if (points[0].Y > Collider.SimPosition.Y) { return LimbStairCollisionResponse.DontClimbStairs; }
803 
804  //5. in water
805  if (inWater && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; }
806 
807  return LimbStairCollisionResponse.ClimbWithLimbCollision;
808  }
809  }
810 
811  lock (impactQueue)
812  {
813  impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
814  }
815 
816  return true;
817  }
818 
819  private void ApplyImpact(Fixture f1, Fixture f2, Vector2 localNormal, Vector2 impactPos, Vector2 velocity)
820  {
821  if (character.DisableImpactDamageTimer > 0.0f) { return; }
822 
823  if (f2.Body?.UserData is Item)
824  {
825  //no impact damage from items
826  //items that can impact characters (melee weapons, projectiles) should handle the damage themselves
827  return;
828  }
829 
830  Vector2 normal = localNormal;
831  float impact = Vector2.Dot(velocity, -normal);
832  if (f1.Body == Collider.FarseerBody || !Collider.Enabled)
833  {
834  bool isNotRemote = true;
835  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { isNotRemote = !character.IsRemotelyControlled; }
836 
837  if (isNotRemote)
838  {
839  float impactTolerance = ImpactTolerance;
840  if (character.Stun > 0.0f) { impactTolerance *= 0.5f; }
841  if (impact > impactTolerance)
842  {
843  impactPos = ConvertUnits.ToDisplayUnits(impactPos);
844  if (character.Submarine != null) { impactPos += character.Submarine.Position; }
845 
846  float impactDamage = GetImpactDamage(impact, impactTolerance);
847 
848  var should = GameMain.LuaCs.Hook.Call<float?>("changeFallDamage", impactDamage, character, impactPos, velocity);
849 
850  if (should != null)
851  {
852  impactDamage = should.Value;
853  }
854 
856  character.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), 0.0f, true);
857  strongestImpact = Math.Max(strongestImpact, impact - impactTolerance);
858  character.ApplyStatusEffects(ActionType.OnImpact, 1.0f);
859  //briefly disable impact damage
860  //otherwise the character will take damage multiple times when for example falling,
861  //because we use the velocity of the collider to determine the impact
862  //(i.e. the character would take damage until the collider hits the floor and stops)
864  }
865  }
866  }
867 
868  ImpactProjSpecific(impact, f1.Body);
869  }
870 
871  public float GetImpactDamage(float impact, float? impactTolerance = null)
872  {
873  float tolerance = impactTolerance ?? ImpactTolerance;
874  return Math.Min((impact - tolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage);
875  }
876 
877  private readonly List<Limb> connectedLimbs = new List<Limb>();
878  private readonly List<LimbJoint> checkedJoints = new List<LimbJoint>();
879  public bool SeverLimbJoint(LimbJoint limbJoint)
880  {
881  if (!limbJoint.CanBeSevered || limbJoint.IsSevered)
882  {
883  return false;
884  }
885 
886  limbJoint.IsSevered = true;
887  limbJoint.Enabled = false;
888 
889  Vector2 limbDiff = limbJoint.LimbA.SimPosition - limbJoint.LimbB.SimPosition;
890  if (limbDiff.LengthSquared() < 0.0001f) { limbDiff = Rand.Vector(1.0f); }
891  limbDiff = Vector2.Normalize(limbDiff);
892  float mass = limbJoint.BodyA.Mass + limbJoint.BodyB.Mass;
893  limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * Math.Min(mass, limbJoint.BodyA.Mass * 500), (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
894  limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * Math.Min(mass, limbJoint.BodyB.Mass * 500), (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
895 
896  connectedLimbs.Clear();
897  checkedJoints.Clear();
898  GetConnectedLimbs(connectedLimbs, checkedJoints, MainLimb);
899  foreach (Limb limb in Limbs)
900  {
901  if (connectedLimbs.Contains(limb)) { continue; }
902  limb.IsSevered = true;
903  if (limb.type == LimbType.RightHand)
904  {
906  }
907  else if (limb.type == LimbType.LeftHand)
908  {
910  }
911  }
912 
913  if (!string.IsNullOrEmpty(character.BloodDecalName))
914  {
916  (limbJoint.LimbA.WorldPosition + limbJoint.LimbB.WorldPosition) / 2, MathHelper.Clamp(Math.Min(limbJoint.LimbA.Mass, limbJoint.LimbB.Mass), 0.5f, 2.0f), isNetworkEvent: false);
917  }
918 
919  SeverLimbJointProjSpecific(limbJoint, playSound: true);
920  if (GameMain.NetworkMember is { IsServer: true })
921  {
923  }
924  return true;
925  }
926 
927  partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound);
928 
929  protected List<Limb> GetConnectedLimbs(Limb limb)
930  {
931  connectedLimbs.Clear();
932  checkedJoints.Clear();
933  GetConnectedLimbs(connectedLimbs, checkedJoints, limb);
934  return connectedLimbs;
935  }
936 
937  private void GetConnectedLimbs(List<Limb> connectedLimbs, List<LimbJoint> checkedJoints, Limb limb)
938  {
939  connectedLimbs.Add(limb);
940 
941  foreach (LimbJoint joint in LimbJoints)
942  {
943  if (joint.IsSevered || checkedJoints.Contains(joint)) { continue; }
944  if (joint.LimbA == limb)
945  {
946  if (!connectedLimbs.Contains(joint.LimbB))
947  {
948  checkedJoints.Add(joint);
949  GetConnectedLimbs(connectedLimbs, checkedJoints, joint.LimbB);
950  }
951  }
952  else if (joint.LimbB == limb)
953  {
954  if (!connectedLimbs.Contains(joint.LimbA))
955  {
956  checkedJoints.Add(joint);
957  GetConnectedLimbs(connectedLimbs, checkedJoints, joint.LimbA);
958  }
959  }
960  }
961  }
962 
963  partial void ImpactProjSpecific(float impact, Body body);
964 
965  public bool IsFlipped { get; private set; }
966 
967  public virtual void Flip()
968  {
969  IsFlipped = !IsFlipped;
970  dir = (dir == Direction.Left) ? Direction.Right : Direction.Left;
971 
972  for (int i = 0; i < LimbJoints.Length; i++)
973  {
974  float lowerLimit = -LimbJoints[i].UpperLimit;
975  float upperLimit = -LimbJoints[i].LowerLimit;
976 
977  LimbJoints[i].LowerLimit = lowerLimit;
978  LimbJoints[i].UpperLimit = upperLimit;
979 
980  LimbJoints[i].LocalAnchorA = new Vector2(-LimbJoints[i].LocalAnchorA.X, LimbJoints[i].LocalAnchorA.Y);
981  LimbJoints[i].LocalAnchorB = new Vector2(-LimbJoints[i].LocalAnchorB.X, LimbJoints[i].LocalAnchorB.Y);
982  }
983 
984  foreach (Limb limb in Limbs)
985  {
986  if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; }
987  limb.Dir = Dir;
988  limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y);
989  limb.MirrorPullJoint();
990  }
991 
992  FlipProjSpecific();
993  }
994 
995  partial void FlipProjSpecific();
996 
997  public Vector2 GetCenterOfMass()
998  {
999  //all limbs disabled -> use the position of the collider
1000  if (!Limbs.Any(l => !l.IsSevered && l.body.Enabled))
1001  {
1002  return Collider.SimPosition;
1003  }
1004 
1005  Vector2 centerOfMass = Vector2.Zero;
1006  float totalMass = 0.0f;
1007  foreach (Limb limb in Limbs)
1008  {
1009  if (limb.IsSevered || !limb.body.Enabled) continue;
1010  centerOfMass += limb.Mass * limb.SimPosition;
1011  totalMass += limb.Mass;
1012  }
1013 
1014  if (totalMass <= 0.0f) return Collider.SimPosition;
1015  centerOfMass /= totalMass;
1016 
1017  if (!MathUtils.IsValid(centerOfMass))
1018  {
1019  string errorMsg = "Ragdoll.GetCenterOfMass returned an invalid value (" + centerOfMass + "). Limb positions: {"
1020  + string.Join(", ", limbs.Select(l => l.SimPosition)) + "}, total mass: " + totalMass + ".";
1021  DebugConsole.ThrowError(errorMsg);
1022  GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1023  return Collider.SimPosition;
1024  }
1025 
1026  return centerOfMass;
1027  }
1028 
1029 
1031  public void MoveLimb(Limb limb, Vector2 pos, float amount, bool pullFromCenter = false)
1032  {
1033  limb.MoveToPos(pos, amount, pullFromCenter);
1034  }
1035 
1036  public void ResetPullJoints(Func<Limb, bool> condition = null)
1037  {
1038  for (int i = 0; i < Limbs.Length; i++)
1039  {
1040  if (Limbs[i] == null) { continue; }
1041  if (condition != null && !condition(Limbs[i])) { continue; }
1042  Limbs[i].PullJointEnabled = false;
1043  }
1044  }
1045 
1046  public static void UpdateAll(float deltaTime, Camera cam)
1047  {
1048  foreach (Ragdoll r in list)
1049  {
1050  r.UpdateRagdoll(deltaTime, cam);
1051  }
1052  }
1053 
1054  public void FindHull(Vector2? worldPosition = null, bool setSubmarine = true)
1055  {
1056  Vector2 findPos = worldPosition == null ? this.WorldPosition : (Vector2)worldPosition;
1057  if (!MathUtils.IsValid(findPos))
1058  {
1059  GameAnalyticsManager.AddErrorEventOnce(
1060  "Ragdoll.FindHull:InvalidPosition",
1061  GameAnalyticsManager.ErrorSeverity.Error,
1062  "Attempted to find a hull at an invalid position (" + findPos + ")\n" + Environment.StackTrace.CleanupStackTrace());
1063  return;
1064  }
1065 
1066  Hull newHull = Hull.FindHull(findPos, currentHull);
1067 
1068  if (newHull == currentHull) { return; }
1069 
1070  if (CanEnterSubmarine == CanEnterSubmarine.False ||
1072  {
1073  //character is inside the sub even though it shouldn't be able to enter -> teleport it out
1074 
1075  //far from an ideal solution, but monsters getting lodged inside the sub seems to be
1076  //pretty rare during normal gameplay (requires abnormally high velocities), so I think
1077  //this is preferable to the cost of using continuous collision detection for the character collider
1078  if (newHull?.Submarine != null)
1079  {
1080  Vector2 hullDiff = WorldPosition - newHull.WorldPosition;
1081  Vector2 moveDir = hullDiff.LengthSquared() < 0.001f ? Vector2.UnitY : Vector2.Normalize(hullDiff);
1082 
1083  //find a position 32 units away from the hull
1084  if (MathUtils.GetLineRectangleIntersection(
1085  newHull.WorldPosition,
1086  newHull.WorldPosition + moveDir * Math.Max(newHull.Rect.Width, newHull.Rect.Height),
1087  new Rectangle(newHull.WorldRect.X - 32, newHull.WorldRect.Y + 32, newHull.WorldRect.Width + 64, newHull.Rect.Height + 64),
1088  out Vector2 intersection))
1089  {
1090  Collider.SetTransform(ConvertUnits.ToSimUnits(intersection), Collider.Rotation);
1091  }
1092  return;
1093  }
1094  }
1095 
1097  {
1098  return;
1099  }
1100 
1101  if (setSubmarine)
1102  {
1103  //in -> out
1104  if (newHull?.Submarine == null && currentHull?.Submarine != null)
1105  {
1106  //don't teleport out yet if the character is going through a gap
1107  if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f, allowRoomToRoom: true) != null) { return; }
1108  if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) { return; }
1109  character.MemLocalState?.Clear();
1111  }
1112  //out -> in
1113  else if (currentHull == null && newHull.Submarine != null)
1114  {
1115  character.MemLocalState?.Clear();
1116  Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity);
1117  }
1118  //from one sub to another
1119  else if (newHull != null && currentHull != null && newHull.Submarine != currentHull.Submarine)
1120  {
1121  character.MemLocalState?.Clear();
1122  Vector2 newSubPos = newHull.Submarine == null ? Vector2.Zero : newHull.Submarine.Position;
1123  Vector2 prevSubPos = currentHull.Submarine == null ? Vector2.Zero : currentHull.Submarine.Position;
1124  Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero);
1125  }
1126  }
1127 
1128  CurrentHull = newHull;
1130  foreach (var attachedProjectile in character.AttachedProjectiles)
1131  {
1132  attachedProjectile.Item.CurrentHull = currentHull;
1133  attachedProjectile.Item.Submarine = character.Submarine;
1134  attachedProjectile.Item.UpdateTransform();
1135  }
1136  }
1137 
1138  private void PreventOutsideCollision()
1139  {
1140  if (currentHull?.Submarine == null) { return; }
1141 
1142  var connectedGaps = currentHull.ConnectedGaps.Where(g => !g.IsRoomToRoom);
1143  foreach (Gap gap in connectedGaps)
1144  {
1145  if (gap.IsHorizontal)
1146  {
1147  if (character.Position.Y > gap.Rect.Y || character.Position.Y < gap.Rect.Y - gap.Rect.Height) continue;
1148  if (Math.Sign(gap.Rect.Center.X - currentHull.Rect.Center.X) !=
1149  Math.Sign(character.Position.X - currentHull.Rect.Center.X))
1150  {
1151  continue;
1152  }
1153  }
1154  else
1155  {
1156  if (character.Position.X < gap.Rect.X || character.Position.X > gap.Rect.Right) continue;
1157  if (Math.Sign((gap.Rect.Y - gap.Rect.Height / 2) - (currentHull.Rect.Y - currentHull.Rect.Height / 2)) !=
1158  Math.Sign(character.Position.Y - (currentHull.Rect.Y - currentHull.Rect.Height / 2)))
1159  {
1160  continue;
1161  }
1162  }
1163 
1164  gap.RefreshOutsideCollider();
1165  }
1166  }
1167 
1168  public void Teleport(Vector2 moveAmount, Vector2 velocityChange, bool detachProjectiles = true)
1169  {
1170  foreach (Limb limb in Limbs)
1171  {
1172  if (limb.IsSevered) { continue; }
1173  if (limb.body.FarseerBody.ContactList == null) { continue; }
1174 
1175  ContactEdge ce = limb.body.FarseerBody.ContactList;
1176  while (ce != null && ce.Contact != null)
1177  {
1178  ce.Contact.Enabled = false;
1179  ce = ce.Next;
1180  }
1181  }
1182 
1183  foreach (Limb limb in Limbs)
1184  {
1185  if (limb.IsSevered) { continue; }
1186  limb.body.LinearVelocity += velocityChange;
1187  }
1188 
1190 
1191  SetPosition(Collider.SimPosition + moveAmount);
1192  character.CursorPosition += moveAmount;
1193 
1195  foreach (Limb limb in Limbs)
1196  {
1197  limb.body.UpdateDrawPosition();
1198  }
1199  }
1200 
1201  private void UpdateCollisionCategories()
1202  {
1203  Category wall = currentHull?.Submarine == null ?
1204  Physics.CollisionLevel | Physics.CollisionWall
1205  : Physics.CollisionWall;
1206 
1207  Category collisionCategory = (IgnorePlatforms) ?
1208  wall | Physics.CollisionProjectile | Physics.CollisionStairs
1209  : wall | Physics.CollisionProjectile | Physics.CollisionPlatform | Physics.CollisionStairs;
1210 
1211  if (collisionCategory == prevCollisionCategory) { return; }
1212  prevCollisionCategory = collisionCategory;
1213 
1214  Collider.CollidesWith = collisionCategory | Physics.CollisionItemBlocking;
1215 
1216  foreach (Limb limb in Limbs)
1217  {
1218  if (limb.IgnoreCollisions || limb.IsSevered) { continue; }
1219 
1220  try
1221  {
1222  limb.body.CollidesWith = collisionCategory;
1223  }
1224  catch (Exception e)
1225  {
1226  DebugConsole.ThrowError("Failed to update ragdoll limb collisioncategories", e);
1227  }
1228  }
1229  }
1230 
1231  protected bool levitatingCollider = true;
1232 
1236  private float bodyInRestTimer;
1237 
1238  private float BodyInRestDelay = 1.0f;
1239 
1240  public bool BodyInRest
1241  {
1242  get { return bodyInRestTimer > BodyInRestDelay; }
1243  set
1244  {
1245  foreach (Limb limb in Limbs)
1246  {
1247  limb.body.PhysEnabled = !value;
1248  }
1249  bodyInRestTimer = value ? BodyInRestDelay : 0.0f;
1250  }
1251  }
1252 
1253  public bool forceStanding;
1254  public bool forceNotStanding;
1255 
1256  public void UpdateRagdoll(float deltaTime, Camera cam)
1257  {
1258  if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; }
1259 
1260  while (impactQueue.Count > 0)
1261  {
1262  var impact = impactQueue.Dequeue();
1263  ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity);
1264  }
1265 
1266  CheckValidity();
1267 
1268  UpdateNetPlayerPosition(deltaTime);
1270  UpdateCollisionCategories();
1271 
1272  FindHull();
1273  PreventOutsideCollision();
1274 
1275  CheckBodyInRest(deltaTime);
1276 
1277  splashSoundTimer -= deltaTime;
1278 
1279  if (character.Submarine == null && Level.Loaded != null)
1280  {
1281  if (Collider.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y)
1282  {
1283  Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, Math.Min(Collider.LinearVelocity.Y, -1));
1284  }
1285  else if (Collider.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y)
1286  {
1288  MathHelper.Clamp(Collider.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - Collider.SimPosition.Y, 10.0f));
1289  }
1290  foreach (Limb limb in Limbs)
1291  {
1292  if (limb.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y)
1293  {
1294  limb.body.LinearVelocity = new Vector2(limb.LinearVelocity.X, Math.Min(limb.LinearVelocity.Y, -1));
1295  }
1296  else if (limb.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y)
1297  {
1298  limb.body.LinearVelocity = new Vector2(
1299  limb.LinearVelocity.X,
1300  MathHelper.Clamp(limb.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - limb.SimPosition.Y, 10.0f));
1301  }
1302  }
1303  }
1304 
1305  if (forceStanding)
1306  {
1307  inWater = false;
1308  headInWater = false;
1309  RefreshFloorY(deltaTime, ignoreStairs: Stairs == null);
1310  }
1311  //ragdoll isn't in any room -> it's in the water
1312  else if (currentHull == null)
1313  {
1314  inWater = true;
1315  headInWater = true;
1316  }
1317  else
1318  {
1319  headInWater = false;
1320  inWater = false;
1321  RefreshFloorY(deltaTime, ignoreStairs: Stairs == null);
1322  if (currentHull.WaterPercentage > 0.001f)
1323  {
1324  (float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY();
1325  float waterSurfaceY = ConvertUnits.ToSimUnits(waterSurfaceDisplayUnits);
1326  float ceilingY = ConvertUnits.ToSimUnits(ceilingDisplayUnits);
1327  if (targetMovement.Y < 0.0f)
1328  {
1329  Vector2 colliderBottom = GetColliderBottom();
1330  floorY = Math.Min(colliderBottom.Y, floorY);
1331  //check if the bottom of the collider is below the current hull
1332  if (floorY < ConvertUnits.ToSimUnits(currentHull.Rect.Y - currentHull.Rect.Height))
1333  {
1334  //set floorY to the position of the floor in the hull below the character
1335  var lowerHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(colliderBottom), useWorldCoordinates: false);
1336  if (lowerHull != null)
1337  {
1338  floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height);
1339  }
1340  }
1341  }
1342  float standHeight = HeadPosition ?? TorsoPosition ?? Collider.GetMaxExtent() * 0.5f;
1343  if (Collider.SimPosition.Y < waterSurfaceY)
1344  {
1345  //too deep to stand up, or not enough room to stand up
1346  if (waterSurfaceY - floorY > standHeight * 0.8f ||
1347  ceilingY - floorY < standHeight * 0.8f)
1348  {
1349  inWater = true;
1350  }
1351  }
1352  }
1353  }
1354 
1355  UpdateHullFlowForces(deltaTime);
1356 
1357  if (currentHull == null ||
1359  ConvertUnits.ToSimUnits(currentHull.Surface) > Collider.SimPosition.Y)
1360  {
1362  }
1363 
1364  foreach (Limb limb in Limbs)
1365  {
1366  //find the room which the limb is in
1367  //the room where the ragdoll is in is used as the "guess", meaning that it's checked first
1368  Hull newHull = currentHull == null ? null : Hull.FindHull(limb.WorldPosition, currentHull);
1369 
1370  bool prevInWater = limb.InWater;
1371  limb.InWater = false;
1372 
1373  if (forceStanding)
1374  {
1375  limb.InWater = false;
1376  }
1377  else if (newHull == null)
1378  {
1379  //limb isn't in any room -> it's in the water
1380  limb.InWater = true;
1381  if (limb.type == LimbType.Head) { headInWater = true; }
1382  }
1383  else if (newHull.WaterVolume > 0.0f && Submarine.RectContains(newHull.Rect, limb.Position))
1384  {
1385  if (limb.Position.Y < newHull.Surface)
1386  {
1387  limb.InWater = true;
1388  surfaceY = newHull.Surface;
1389  if (limb.type == LimbType.Head)
1390  {
1391  headInWater = true;
1392  }
1393  }
1394  //the limb has gone through the surface of the water
1395  if (Math.Abs(limb.LinearVelocity.Y) > 5.0f && limb.InWater != prevInWater && newHull == limb.Hull)
1396  {
1397  Splash(limb, newHull);
1398  //if the Character dropped into water, create a wave
1399  if (limb.LinearVelocity.Y < 0.0f)
1400  {
1401  Vector2 impulse = limb.LinearVelocity * limb.Mass;
1402  int n = (int)((limb.Position.X - newHull.Rect.X) / Hull.WaveWidth);
1403  newHull.WaveVel[n] += MathHelper.Clamp(impulse.Y, -5.0f, 5.0f);
1404  }
1405  }
1406  }
1407  limb.Hull = newHull;
1408  limb.Update(deltaTime);
1409  }
1410 
1412  {
1414  {
1415  float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor;
1416 
1417  const float LevitationSpeedMultiplier = 5f;
1418 
1419  // If the character is walking down a slope, target a position that moves along it
1420  float slopePull = 0f;
1421  if (floorNormal.Y is > 0f and < 1f
1422  && Math.Sign(movement.X) == Math.Sign(floorNormal.X))
1423  {
1424  float steepness = Math.Abs(floorNormal.X);
1425  slopePull = Math.Abs(movement.X * steepness) / LevitationSpeedMultiplier;
1426  }
1427 
1428  if (Math.Abs(Collider.SimPosition.Y - targetY - slopePull) > 0.01f)
1429  {
1430  float yVelocity = (targetY - Collider.SimPosition.Y) * LevitationSpeedMultiplier;
1431  if (Stairs != null && targetY < Collider.SimPosition.Y)
1432  {
1433  yVelocity = Math.Sign(yVelocity);
1434  }
1435 
1436  yVelocity -= slopePull * LevitationSpeedMultiplier;
1437 
1438  Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, yVelocity);
1439  }
1440  }
1441  else
1442  {
1443  // Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck.
1444  if (Collider.LinearVelocity == Vector2.Zero)
1445  {
1446  character.IsRagdolled = true;
1447  if (character.IsBot)
1448  {
1449  // Seems to work without this on player controlled characters -> not sure if we should call it always or just for the bots.
1450  character.SetInput(InputType.Ragdoll, hit: false, held: true);
1451  }
1452  }
1453  }
1454  }
1455  UpdateProjSpecific(deltaTime, cam);
1456  forceNotStanding = false;
1457  }
1458 
1459  private void CheckBodyInRest(float deltaTime)
1460  {
1461  if (SimplePhysicsEnabled) { return; }
1462 
1463  if (InWater || Collider.LinearVelocity.LengthSquared() > 0.01f || character.SelectedBy != null || !character.IsDead)
1464  {
1465  bodyInRestTimer = 0.0f;
1466  foreach (Limb limb in Limbs)
1467  {
1468  limb.body.PhysEnabled = true;
1469  }
1470  }
1471  else if (Limbs.All(l => l != null && !l.body.Enabled || l.LinearVelocity.LengthSquared() < 0.001f))
1472  {
1473  bodyInRestTimer += deltaTime;
1474  if (bodyInRestTimer > BodyInRestDelay)
1475  {
1476  foreach (Limb limb in Limbs)
1477  {
1478  limb.body.PhysEnabled = false;
1479  }
1480  }
1481  }
1482  }
1483 
1484  public bool Invalid { get; private set; }
1485  private int validityResets;
1486  private bool CheckValidity()
1487  {
1488  if (limbs == null)
1489  {
1490  DebugConsole.ThrowError("Attempted to check the validity of a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this));
1491  Invalid = true;
1492  return false;
1493  }
1494  bool isColliderValid = CheckValidity(Collider);
1495  if (!isColliderValid) { Collider.ResetDynamics(); }
1496  bool limbsValid = true;
1497  foreach (Limb limb in limbs)
1498  {
1499  if (limb?.body == null || !limb.body.Enabled) { continue; }
1500  if (!CheckValidity(limb.body))
1501  {
1502  limbsValid = false;
1503  limb.body.ResetDynamics();
1504  break;
1505  }
1506  }
1507  bool isValid = isColliderValid && limbsValid;
1508  if (!isValid)
1509  {
1510  validityResets++;
1511  if (validityResets > 3)
1512  {
1513  Invalid = true;
1514  DebugConsole.ThrowError("Invalid ragdoll physics. Ragdoll frozen to prevent crashes.");
1515  Collider.SetTransform(Vector2.Zero, 0.0f);
1517  foreach (Limb limb in Limbs)
1518  {
1519  limb.body?.SetTransform(Collider.SimPosition, 0.0f);
1520  limb.body?.ResetDynamics();
1521  }
1522  Frozen = true;
1523  }
1524  }
1525  return isValid;
1526  }
1527 
1528  private bool CheckValidity(PhysicsBody body)
1529  {
1530  string errorMsg = null;
1531  if (!MathUtils.IsValid(body.SimPosition) || Math.Abs(body.SimPosition.X) > 1e10f || Math.Abs(body.SimPosition.Y) > 1e10f)
1532  {
1533  errorMsg = GetBodyName() + " position invalid (" + body.SimPosition + ", character: [name]).";
1534  }
1535  else if (!MathUtils.IsValid(body.LinearVelocity) || Math.Abs(body.LinearVelocity.X) > 1000f || Math.Abs(body.LinearVelocity.Y) > 1000f)
1536  {
1537  errorMsg = GetBodyName() + " velocity invalid (" + body.LinearVelocity + ", character: [name]).";
1538  }
1539  else if (!MathUtils.IsValid(body.Rotation))
1540  {
1541  errorMsg = GetBodyName() + " rotation invalid (" + body.Rotation + ", character: [name]).";
1542  }
1543  else if (!MathUtils.IsValid(body.AngularVelocity) || Math.Abs(body.AngularVelocity) > 1000f)
1544  {
1545  errorMsg = GetBodyName() + " angular velocity invalid (" + body.AngularVelocity + ", character: [name]).";
1546  }
1547  if (errorMsg != null)
1548  {
1550  {
1551  errorMsg += " Ragdoll controlled remotely.";
1552  }
1554  {
1555  errorMsg += " Simple physics enabled.";
1556  }
1557  if (GameMain.NetworkMember != null)
1558  {
1559  errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server.";
1560  }
1561 
1562 #if DEBUG
1563  DebugConsole.ThrowError(errorMsg.Replace("[name]", Character.Name));
1564 #else
1565  DebugConsole.NewMessage(errorMsg.Replace("[name]", Character.Name), Color.Red);
1566 #endif
1567  GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", Character.SpeciesName.Value));
1568 
1569  if (!MathUtils.IsValid(Collider.SimPosition) || Math.Abs(Collider.SimPosition.X) > 1e10f || Math.Abs(Collider.SimPosition.Y) > 1e10f)
1570  {
1571  Collider.SetTransform(Vector2.Zero, 0.0f);
1572  }
1573  foreach (Limb otherLimb in Limbs)
1574  {
1575  otherLimb.body.SetTransform(Collider.SimPosition, 0.0f);
1576  otherLimb.body.ResetDynamics();
1577  }
1578  SetInitialLimbPositions();
1579  return false;
1580  }
1581 
1582  string GetBodyName()
1583  {
1584  return body.UserData is Limb limb ? "Limb (" + limb.type + ")" : "Collider";
1585  }
1586 
1587  return true;
1588  }
1589 
1591  {
1592  if (!accessRemovedCharacterErrorShown)
1593  {
1594  string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
1595  errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
1596  DebugConsole.ThrowError(errorMsg);
1597  GameAnalyticsManager.AddErrorEventOnce(
1598  "Ragdoll:AccessRemoved",
1599  GameAnalyticsManager.ErrorSeverity.Error,
1600  "Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
1601  accessRemovedCharacterErrorShown = true;
1602  }
1603  }
1604 
1605  partial void UpdateProjSpecific(float deltaTime, Camera cam);
1606 
1607  partial void Splash(Limb limb, Hull limbHull);
1608 
1609  private void UpdateHullFlowForces(float deltaTime)
1610  {
1611  if (currentHull == null) { return; }
1612 
1613  const float StunForceThreshold = 5.0f;
1614  const float StunDuration = 0.5f;
1615  const float ToleranceIncreaseSpeed = 5.0f;
1616  const float ToleranceDecreaseSpeed = 1.0f;
1617 
1618  //how much distance to a gap affects the force it exerts on the character
1619  const float DistanceFactor = 0.5f;
1620  const float ForceMultiplier = 0.035f;
1621 
1622  Vector2 flowForce = Vector2.Zero;
1623  foreach (Gap gap in Gap.GapList)
1624  {
1625  if (gap.Open <= 0.0f || !gap.linkedTo.Contains(currentHull) || gap.LerpedFlowForce.LengthSquared() < 0.01f) { continue; }
1626  float dist = Vector2.Distance(MainLimb.WorldPosition, gap.WorldPosition) * DistanceFactor;
1627  flowForce += Vector2.Normalize(gap.LerpedFlowForce) * (Math.Max(gap.LerpedFlowForce.Length() - dist, 0.0f) * ForceMultiplier);
1628  }
1629 
1630  //throwing conscious/moving characters around takes more force -> double the flow force
1631  if (character.CanMove) { flowForce *= 2.0f; }
1632  flowForce *= 1 - Math.Clamp(character.GetStatValue(StatTypes.FlowResistance), 0f, 1f);
1633 
1634  float flowForceMagnitude = flowForce.Length();
1635  float limbMultipier = limbs.Count(l => l.InWater) / (float)limbs.Length;
1636  //if the force strong enough, stun the character to let it get thrown around by the water
1637  if ((flowForceMagnitude * limbMultipier) - flowStunTolerance > StunForceThreshold)
1638  {
1639  character.Stun = Math.Max(character.Stun, StunDuration);
1640  flowStunTolerance = Math.Max(flowStunTolerance, flowForceMagnitude);
1641  }
1642 
1643  if (character == Character.Controlled && inWater && Screen.Selected?.Cam != null)
1644  {
1645  float shakeStrength = Math.Min(flowForceMagnitude / 10.0f, 5.0f) * limbMultipier;
1646  Screen.Selected.Cam.Shake = Math.Max(Screen.Selected.Cam.Shake, shakeStrength);
1647  }
1648 
1649  if (flowForceMagnitude > 0.0001f)
1650  {
1651  flowForce = Vector2.Normalize(flowForce) * Math.Max(flowForceMagnitude - flowForceTolerance, 0.0f);
1652  }
1653 
1654  if (flowForceTolerance <= flowForceMagnitude * 1.5f && inWater)
1655  {
1656  //build up "tolerance" to the flow force
1657  //ensures the character won't get permanently stuck by forces, while allowing sudden changes in flow to push the character hard
1658  flowForceTolerance += deltaTime * ToleranceIncreaseSpeed;
1659  flowStunTolerance = Math.Max(flowStunTolerance, flowForceTolerance);
1660  }
1661  else
1662  {
1663  flowForceTolerance = Math.Max(flowForceTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f);
1664  flowStunTolerance = Math.Max(flowStunTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f);
1665  }
1666 
1667  if (flowForce.LengthSquared() > 0.001f)
1668  {
1669  Collider.ApplyForce(flowForce * (Collider.Mass / Mass));
1670  foreach (Limb limb in limbs)
1671  {
1672  if (!limb.InWater) { continue; }
1673  limb.body.ApplyForce(flowForce * (limb.Mass / Mass * limbs.Length));
1674  }
1675  }
1676  }
1677 
1678  public void ForceRefreshFloorY()
1679  {
1680  lastFloorCheckPos = Vector2.Zero;
1681  }
1682 
1683  // Force check floor y at least once a second so that we'll drop through gaps that we are standing upon.
1684  private const float FloorYStaleTime = 1;
1685  private float floorYCheckTimer;
1686  private void RefreshFloorY(float deltaTime, bool ignoreStairs = false)
1687  {
1688  floorYCheckTimer -= deltaTime;
1689  PhysicsBody refBody = Collider;
1690  if (floorYCheckTimer < 0 ||
1691  lastFloorCheckIgnoreStairs != ignoreStairs ||
1692  lastFloorCheckIgnorePlatforms != IgnorePlatforms ||
1693  Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f)
1694  {
1695  floorY = GetFloorY(refBody.SimPosition, ignoreStairs);
1696  lastFloorCheckPos = refBody.SimPosition;
1697  lastFloorCheckIgnoreStairs = ignoreStairs;
1698  lastFloorCheckIgnorePlatforms = IgnorePlatforms;
1699  // Add some randomness to prevent all stationary characters doing the checks at the same frame.
1700  floorYCheckTimer = FloorYStaleTime * Rand.Range(0.9f, 1.1f);
1701  }
1702  }
1703 
1704  private float GetFloorY(Vector2 simPosition, bool ignoreStairs = false)
1705  {
1706  onGround = false;
1707  Stairs = null;
1708  floorFixture = null;
1709  Vector2 rayStart = simPosition;
1710  float height = ColliderHeightFromFloor;
1711  if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); }
1712  if (TorsoPosition.HasValue && MathUtils.IsValid(TorsoPosition.Value)) { height = Math.Max(height, TorsoPosition.Value); }
1713 
1714  Vector2 rayEnd = rayStart - new Vector2(0.0f, height * 2f);
1715  Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom());
1716 
1717  Fixture standOnFloorFixture = null;
1718  float standOnFloorFraction = 1;
1719  float closestFraction = 1;
1720  GameMain.World.RayCast((fixture, point, normal, fraction) =>
1721  {
1722  switch (fixture.CollisionCategories)
1723  {
1724  case Physics.CollisionStairs:
1725  if (inWater && TargetMovement.Y < 0.5f) { return -1; }
1726 
1727  if (character.SelectedBy == null && fraction < standOnFloorFraction)
1728  {
1729  Structure structure = fixture.Body.UserData as Structure;
1730  if (colliderBottomDisplay.Y >= structure.Rect.Y - structure.Rect.Height + 30 || TargetMovement.Y > 0.5f || Stairs != null)
1731  {
1732  standOnFloorFraction = fraction;
1733  standOnFloorFixture = fixture;
1734  }
1735  }
1736 
1737  if (ignoreStairs) { return -1; }
1738  break;
1739  case Physics.CollisionPlatform:
1740  Structure platform = fixture.Body.UserData as Structure;
1741 
1742  if (!IgnorePlatforms && fraction < standOnFloorFraction)
1743  {
1744  if (colliderBottomDisplay.Y >= platform.Rect.Y - 16 || (targetMovement.Y > 0.0f && Stairs == null))
1745  {
1746  standOnFloorFraction = fraction;
1747  standOnFloorFixture = fixture;
1748  }
1749  }
1750 
1751  if (colliderBottomDisplay.Y < platform.Rect.Y - 16 && (targetMovement.Y <= 0.0f || Stairs != null)) return -1;
1752  if (IgnorePlatforms && TargetMovement.Y < -0.5f || Collider.Position.Y < platform.Rect.Y) return -1;
1753  break;
1754  case Physics.CollisionWall:
1755  case Physics.CollisionLevel:
1756  if (!fixture.CollidesWith.HasFlag(Physics.CollisionCharacter)) { return -1; }
1757  if (fixture.Body.UserData is Submarine && character.Submarine != null) { return -1; }
1758  if (fixture.IsSensor) { return -1; }
1759  if (fraction < standOnFloorFraction)
1760  {
1761  standOnFloorFraction = fraction;
1762  standOnFloorFixture = fixture;
1763  }
1764  break;
1765  default:
1766  System.Diagnostics.Debug.Assert(false, "Floor raycast should not have hit a fixture with the collision category " + fixture.CollisionCategories);
1767  return -1;
1768  }
1769 
1770  if (fraction < closestFraction)
1771  {
1772  floorNormal = normal;
1773  closestFraction = fraction;
1774  }
1775 
1776  return closestFraction;
1777  }, rayStart, rayEnd, Physics.CollisionStairs | Physics.CollisionPlatform | Physics.CollisionWall | Physics.CollisionLevel);
1778 
1779  if (standOnFloorFixture != null && !IsHangingWithRope)
1780  {
1781  floorFixture = standOnFloorFixture;
1782  standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction;
1783  if (rayStart.Y - standOnFloorY < Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor * 1.2f)
1784  {
1785  onGround = true;
1786  if (standOnFloorFixture.CollisionCategories == Physics.CollisionStairs)
1787  {
1788  Stairs = standOnFloorFixture.Body.UserData as Structure;
1789  }
1790  }
1791  }
1792 
1793  if (closestFraction >= 1) //raycast didn't hit anything
1794  {
1795  floorNormal = Vector2.UnitY;
1796  if (CurrentHull == null)
1797  {
1798  return -1000.0f;
1799  }
1800  else
1801  {
1802  float hullBottom = currentHull.Rect.Y - currentHull.Rect.Height;
1803  //check if there's a connected hull below
1804  foreach (var gap in currentHull.ConnectedGaps)
1805  {
1806  if (!gap.IsRoomToRoom || gap.Open < 1.0f || gap.ConnectedDoor != null || gap.IsHorizontal) { continue; }
1807  if (WorldPosition.X > gap.WorldRect.X && WorldPosition.X < gap.WorldRect.Right && gap.WorldPosition.Y < WorldPosition.Y)
1808  {
1809  var lowerHull = gap.linkedTo[0] == currentHull ? gap.linkedTo[1] : gap.linkedTo[0];
1810  hullBottom = Math.Min(hullBottom, lowerHull.Rect.Y - lowerHull.Rect.Height);
1811  }
1812  }
1813  return ConvertUnits.ToSimUnits(hullBottom);
1814  }
1815  }
1816  else
1817  {
1818  return rayStart.Y + (rayEnd.Y - rayStart.Y) * closestFraction;
1819  }
1820  }
1821 
1825  public float GetSurfaceY()
1826  {
1827  return GetWaterSurfaceAndCeilingY().WaterSurfaceY;
1828  }
1829 
1833  private (float WaterSurfaceY, float CeilingY) GetWaterSurfaceAndCeilingY()
1834  {
1835  //check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside
1836  if (currentHull == null || character.CurrentHull == null)
1837  {
1838  return (float.PositiveInfinity, float.PositiveInfinity);
1839  }
1840 
1841  float surfaceY = currentHull.Surface;
1842  float ceilingY = currentHull.Rect.Y;
1843  float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f);
1844  //if the hull is almost full of water, check if there's a water-filled hull above it
1845  //and use its water surface instead of the current hull's
1846  if (currentHull.Rect.Y - currentHull.Surface < 5.0f)
1847  {
1848  GetSurfacePos(currentHull, ref surfaceY, ref ceilingY);
1849  void GetSurfacePos(Hull hull, ref float prevSurfacePos, ref float ceilingPos)
1850  {
1851  if (prevSurfacePos > surfaceThreshold) { return; }
1852  foreach (Gap gap in hull.ConnectedGaps)
1853  {
1854  if (gap.IsHorizontal || gap.Open <= 0.0f || gap.WorldPosition.Y < hull.WorldPosition.Y) { continue; }
1855  if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) { continue; }
1856 
1857  //if the gap is above us and leads outside, there's no surface to limit the movement
1858  if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y)
1859  {
1860  ceilingPos += 100000.0f;
1861  prevSurfacePos += 100000.0f;
1862  return;
1863  }
1864 
1865  foreach (var linkedTo in gap.linkedTo)
1866  {
1867  if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull)
1868  {
1869  prevSurfacePos = Math.Max(surfaceY, otherHull.Surface);
1870  ceilingPos = Math.Max(ceilingPos, otherHull.Rect.Y);
1871  GetSurfacePos(otherHull, ref prevSurfacePos, ref ceilingPos);
1872  break;
1873  }
1874  }
1875  }
1876  }
1877  }
1878  return (surfaceY, ceilingY);
1879  }
1880 
1881  public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool moveLatchers = true)
1882  {
1883  if (!MathUtils.IsValid(simPosition))
1884  {
1885  DebugConsole.ThrowError("Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace());
1886  GameAnalyticsManager.AddErrorEventOnce(
1887  "Ragdoll.SetPosition:InvalidPosition",
1888  GameAnalyticsManager.ErrorSeverity.Error,
1889  "Attempted to move a ragdoll (" + character.SpeciesName + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace());
1890  return;
1891  }
1892  if (MainLimb == null) { return; }
1893 
1894  Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition;
1895 
1896  // A Work-around for an issue with teleporting the characters:
1897  // Detach every latcher when either one of the latchers or the target is teleported,
1898  // because otherwise all the characters are teleported to invalid positions.
1899  const float ForceDeattachThreshold = 10.0f;
1900  if (limbMoveAmount.LengthSquared() > ForceDeattachThreshold * ForceDeattachThreshold &&
1901  Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached)
1902  {
1903  var target = enemyAI.LatchOntoAI.TargetCharacter;
1904  if (target != null)
1905  {
1906  target.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
1907  target.Latchers.Clear();
1908  }
1909  enemyAI.LatchOntoAI.DeattachFromBody(reset: true);
1910  }
1911  Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
1912  Character.Latchers.Clear();
1913 
1914  if (lerp)
1915  {
1916  Collider.TargetPosition = simPosition;
1917  Collider.MoveToTargetPosition(true);
1918  }
1919  else
1920  {
1921  Collider.SetTransform(simPosition, Collider.Rotation);
1922  }
1923 
1924  if (!MathUtils.NearlyEqual(limbMoveAmount, Vector2.Zero))
1925  {
1926  foreach (Limb limb in Limbs)
1927  {
1928  if (limb.IsSevered) { continue; }
1929  //check visibility from the new position of the collider to the new position of this limb
1930  Vector2 movePos = limb.SimPosition + limbMoveAmount;
1931  TrySetLimbPosition(limb, simPosition, movePos, limb.Rotation, lerp, ignorePlatforms);
1932  }
1933  }
1934  }
1935 
1939  public bool IsHoldingToRope { get; private set; }
1940  protected bool shouldHoldToRope;
1941 
1945  public bool IsHangingWithRope { get; private set; }
1946  protected bool shouldHangWithRope;
1947 
1951  public bool IsDraggedWithRope { get; private set; }
1952  protected bool shouldBeDraggedWithRope;
1953 
1954  public void HangWithRope()
1955  {
1956  shouldHangWithRope = true;
1957  IsHangingWithRope = true;
1958  ResetPullJoints();
1959  onGround = false;
1960  levitatingCollider = false;
1961  }
1962 
1963  public void HoldToRope()
1964  {
1965  shouldHoldToRope = true;
1966  IsHoldingToRope = true;
1967  }
1968 
1969  public void DragWithRope()
1970  {
1971  shouldBeDraggedWithRope = true;
1972  IsDraggedWithRope = true;
1973  }
1974 
1975  protected void StopHangingWithRope()
1976  {
1977  shouldHangWithRope = false;
1978  IsHangingWithRope = false;
1979  }
1980 
1981  protected void StopHoldingToRope()
1982  {
1983  shouldHoldToRope = false;
1984  IsHoldingToRope = false;
1985  }
1986 
1988  {
1989  shouldBeDraggedWithRope = false;
1990  IsDraggedWithRope = false;
1991  }
1992 
1993  protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true)
1994  {
1995  Vector2 movePos = simPosition;
1996  Vector2 prevPosition = limb.body.SimPosition;
1997  if (Vector2.DistanceSquared(original, simPosition) > 0.0001f)
1998  {
1999  Category collisionCategory = Physics.CollisionWall | Physics.CollisionLevel;
2000  if (!ignorePlatforms) { collisionCategory |= Physics.CollisionPlatform; }
2001 
2002  Body body = Submarine.PickBody(original, simPosition, null, collisionCategory);
2003 
2004  //if there's something in between the limbs
2005  if (body != null)
2006  {
2007  //move the limb close to the position where the raycast hit something
2008  movePos = original + ((simPosition - original) * Submarine.LastPickedFraction * 0.9f);
2009  }
2010  }
2011 
2012  if (lerp)
2013  {
2014  limb.body.TargetPosition = movePos;
2015  limb.body.TargetRotation = rotation;
2016  limb.body.MoveToTargetPosition(true);
2017  }
2018  else
2019  {
2020  limb.body.SetTransform(movePos, rotation);
2022  limb.PullJointEnabled = false;
2023  }
2024  foreach (var attachedProjectile in character.AttachedProjectiles)
2025  {
2026  if (attachedProjectile.IsAttachedTo(limb.body))
2027  {
2028  attachedProjectile.Item.SetTransform(
2029  attachedProjectile.Item.SimPosition + (movePos - prevPosition),
2030  attachedProjectile.Item.body.Rotation,
2031  findNewHull: false);
2032  }
2033  }
2034  }
2035 
2036 
2037  private bool collisionsDisabled;
2038  private double lastObstacleRayCastTime;
2039 
2040  protected void CheckDistFromCollider()
2041  {
2042  float allowedDist = Math.Max(Math.Max(Collider.Radius, Collider.Width), Collider.Height) * 2.0f;
2043  allowedDist = Math.Max(allowedDist, 1.0f);
2044  float resetDist = allowedDist * 5.0f;
2045 
2046  float obstacleCheckDist = 0.3f;
2047 
2048  Vector2 diff = Collider.SimPosition - MainLimb.SimPosition;
2049  float distSqrd = diff.LengthSquared();
2050 
2051  bool shouldReset = distSqrd > resetDist * resetDist;
2052  if (!shouldReset && distSqrd > obstacleCheckDist * obstacleCheckDist)
2053  {
2054  if (Timing.TotalTime > lastObstacleRayCastTime + 1 &&
2055  Submarine.PickBody(Collider.SimPosition, MainLimb.SimPosition, collisionCategory: Physics.CollisionWall) != null)
2056  {
2057  shouldReset = true;
2058  lastObstacleRayCastTime = Timing.TotalTime;
2059  }
2060  }
2061 
2062  if (shouldReset)
2063  {
2064  //ragdoll way too far, reset position
2065  SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true);
2066  }
2067  else if (distSqrd > allowedDist * allowedDist)
2068  {
2069  //ragdoll too far from the collider, disable collisions until it's close enough
2070  //(in case the ragdoll has gotten stuck somewhere)
2071 
2072  Vector2 forceDir = diff / (float)Math.Sqrt(distSqrd);
2073  foreach (Limb limb in Limbs)
2074  {
2075  if (limb.IsSevered) { continue; }
2076  limb.body.CollidesWith = Physics.CollisionNone;
2077  limb.body.ApplyForce(forceDir * limb.Mass * 10.0f, maxVelocity: 10.0f);
2078  }
2079 
2080  collisionsDisabled = true;
2081  }
2082  else if (collisionsDisabled)
2083  {
2084  //set the position of the ragdoll to make sure limbs don't get stuck inside walls when re-enabling collisions
2085  SetPosition(Collider.SimPosition, lerp: true);
2086  collisionsDisabled = false;
2087  //force collision categories to be updated
2088  prevCollisionCategory = Category.None;
2089  }
2090  }
2091 
2092  partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos);
2093  private void UpdateNetPlayerPosition(float deltaTime)
2094  {
2095  if (GameMain.NetworkMember == null) return;
2096 
2097  float lowestSubPos = float.MaxValue;
2098  if (Submarine.Loaded.Any())
2099  {
2100  lowestSubPos = ConvertUnits.ToSimUnits(Submarine.Loaded.Min(s => s.HiddenSubPosition.Y - s.Borders.Height - 128.0f));
2101  for (int i = 0; i < character.MemState.Count; i++)
2102  {
2103  if (character.Submarine == null)
2104  {
2105  //transform in-sub coordinates to outside coordinates
2106  if (character.MemState[i].Position.Y > lowestSubPos)
2107  character.MemState[i].TransformInToOutside();
2108  }
2109  else if (currentHull?.Submarine != null)
2110  {
2111  //transform outside coordinates to in-sub coordinates
2112  if (character.MemState[i].Position.Y < lowestSubPos)
2113  character.MemState[i].TransformOutToInside(currentHull.Submarine);
2114  }
2115  }
2116  }
2117 
2118  UpdateNetPlayerPositionProjSpecific(deltaTime, lowestSubPos);
2119  }
2120 
2124  public Limb GetLimb(LimbType limbType, bool excludeSevered = true)
2125  {
2126  if (limbDictionary.TryGetValue(limbType, out Limb limb))
2127  {
2128  if (excludeSevered && limb.IsSevered)
2129  {
2130  limb = null;
2131  }
2132  }
2133  if (limb == null && HasMultipleLimbsOfSameType)
2134  {
2135  // Didn't find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs.
2136  foreach (var l in limbs)
2137  {
2138  if (l.type != limbType) { continue; }
2139  if (!excludeSevered || !l.IsSevered)
2140  {
2141  limb = l;
2142  break;
2143  }
2144  }
2145  }
2146  return limb;
2147  }
2148 
2149  public Vector2? GetMouthPosition()
2150  {
2151  Limb mouthLimb = GetLimb(LimbType.Head);
2152  if (mouthLimb == null) { return null; }
2153  float cos = (float)Math.Cos(mouthLimb.Rotation);
2154  float sin = (float)Math.Sin(mouthLimb.Rotation);
2155  Vector2 bodySize = mouthLimb.body.GetSize();
2156  Vector2 offset = new Vector2(mouthLimb.MouthPos.X * bodySize.X / 2, mouthLimb.MouthPos.Y * bodySize.Y / 2);
2157  return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos);
2158  }
2159 
2160  public Vector2 GetColliderBottom()
2161  {
2162  float offset = 0.0f;
2163 
2164  if (!character.IsDead && character.Stun <= 0.0f && !character.IsIncapacitated)
2165  {
2166  offset = -ColliderHeightFromFloor;
2167  }
2168 
2169  float lowestBound = Collider.SimPosition.Y;
2170  if (Collider.FarseerBody.FixtureList != null)
2171  {
2172  for (int i = 0; i < Collider.FarseerBody.FixtureList.Count; i++)
2173  {
2174  Collider.FarseerBody.GetTransform(out FarseerPhysics.Common.Transform transform);
2175  Collider.FarseerBody.FixtureList[i].Shape.ComputeAABB(out FarseerPhysics.Collision.AABB aabb, ref transform, i);
2176 
2177  lowestBound = Math.Min(aabb.LowerBound.Y, lowestBound);
2178  }
2179  }
2180  return new Vector2(Collider.SimPosition.X, lowestBound + offset);
2181  }
2182 
2184  {
2185  Limb lowestLimb = null;
2186  foreach (Limb limb in Limbs)
2187  {
2188  if (limb.IsSevered) { continue; }
2189  if (lowestLimb == null)
2190  {
2191  lowestLimb = limb;
2192  }
2193  else if (limb.SimPosition.Y < lowestLimb.SimPosition.Y)
2194  {
2195  lowestLimb = limb;
2196  }
2197  }
2198 
2199  return lowestLimb;
2200  }
2201 
2202  public void ReleaseStuckLimbs()
2203  {
2204  // Commented out, because stuck limbs is not a feature that we currently use, as it would require that we sync all the limbs, which we don't do.
2205  //Limbs.ForEach(l => l.Release());
2206  }
2207 
2208  public void HideAndDisable(LimbType limbType, float duration = 0, bool ignoreCollisions = true)
2209  {
2210  foreach (var limb in Limbs)
2211  {
2212  if (limb.type == limbType)
2213  {
2214  limb.HideAndDisable(duration, ignoreCollisions);
2215  }
2216  }
2217  }
2218 
2219  public void RestoreTemporarilyDisabled() => Limbs.ForEach(l => l.ReEnable());
2220 
2221  public void Remove()
2222  {
2223  if (Limbs != null)
2224  {
2225  foreach (Limb l in Limbs)
2226  {
2227  l?.Remove();
2228  }
2229  limbs = null;
2230  }
2231 
2232  if (collider != null)
2233  {
2234  foreach (PhysicsBody b in collider)
2235  {
2236  b?.Remove();
2237  }
2238  collider = null;
2239  }
2240 
2241  if (LimbJoints != null)
2242  {
2243  foreach (var joint in LimbJoints)
2244  {
2245  var j = joint?.Joint;
2246  if (GameMain.World.JointList.Contains(j))
2247  {
2248  GameMain.World.Remove(j);
2249  }
2250  }
2251  LimbJoints = null;
2252  }
2253 
2254  list.Remove(this);
2255  }
2256 
2257  public static void RemoveAll()
2258  {
2259  for (int i = list.Count - 1; i >= 0; i--)
2260  {
2261  list[i].Remove();
2262  }
2263  System.Diagnostics.Debug.Assert(list.Count == 0, "Some ragdolls were not removed in Ragdoll.RemoveAll");
2264  }
2265  }
2266 }
A special affliction type that gradually makes the character turn into another type of character....
static List< Limb > AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, ContentXElement appendageDefinition=null, Ragdoll ragdoll=null)
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
static readonly PrefabCollection< AfflictionPrefab > Prefabs
AfflictionPrefabHusk is a special type of affliction that has added functionality for husk infection.
void SetInput(InputType inputType, bool hit, bool held)
bool IsRemotelyControlled
Is the character controlled remotely (either by another player, or a server-side AIController)
float GetStatValue(StatTypes statType, bool includeSaved=true)
void ApplyStatusEffects(ActionType actionType, float deltaTime)
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound=true)
static CharacterPrefab FindByFilePath(string filePath)
ContentXElement ConfigElement
Identifier GetAttributeIdentifier(string key, string def)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
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
static CharacterEditor.CharacterEditorScreen CharacterEditorScreen
Definition: GameMain.cs:74
static ContentPackage VanillaContent
Definition: GameMain.cs:84
static World World
Definition: GameMain.cs:105
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static Gap FindAdjacent(IEnumerable< Gap > gaps, Vector2 worldPos, float allowedOrthogonalDist, bool allowRoomToRoom=false)
Decal AddDecal(UInt32 decalId, Vector2 worldPosition, float scale, bool isNetworkEvent, int? spriteIndex=null)
static Hull FindHull(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any)
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
LatchOntoAI(XElement element, EnemyAIController enemyAI)
Definition: LatchOntoAI.cs:63
readonly List< WearableSprite > WearingItems
void MoveToPos(Vector2 pos, float force, bool pullFromCenter=false)
void HideAndDisable(float duration=0, bool ignoreCollisions=true)
static bool IsValidShape(float radius, float height, float width)
void ApplyForce(Vector2 force, float maxVelocity=NetConfig.MaxPhysicsBodyVelocity)
bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransform=true)
void ApplyWaterForces()
Applies buoyancy, drag and angular drag caused by water
void FindHull(Vector2? worldPosition=null, bool setSubmarine=true)
bool TryGetCollider(int index, out PhysicsBody collider)
void ResetJoints()
Resets the current joint values to the serialized joint params.
void ResetLimbs()
Resets the current limb values to the serialized limb params.
void ResetPullJoints(Func< Limb, bool > condition=null)
Ragdoll(Character character, string seed, RagdollParams ragdollParams=null)
bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact)
void SaveRagdoll(string fileNameWithoutExtension=null)
Saves all serializable data in the currently selected ragdoll params. This method should properly han...
void SetPosition(Vector2 simPosition, bool lerp=false, bool ignorePlatforms=true, bool forceMainLimbToCollider=false, bool moveLatchers=true)
void RestoreTemporarilyDisabled()
void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp=false, bool ignorePlatforms=true)
float GetImpactDamage(float impact, float? impactTolerance=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.
float GetSurfaceY()
Get the position of the surface of water at the position of the character, in display units (taking i...
float ColliderHeightFromFloor
In sim units. Joint scale applied.
void ResetRagdoll(bool forceReload=false)
Resets the serializable data to the currently selected ragdoll params. Force reloading always loads t...
virtual void Recreate(RagdollParams ragdollParams=null)
Call this to create the ragdoll from the RagdollParams.
void Teleport(Vector2 moveAmount, Vector2 velocityChange, bool detachProjectiles=true)
void MoveLimb(Limb limb, Vector2 pos, float amount, bool pullFromCenter=false)
if false, force is applied to the position of pullJoint
void HideAndDisable(LimbType limbType, float duration=0, bool ignoreCollisions=true)
override bool Reset(bool forceReload=false)
Resets the current properties to the xml (stored in memory). Force reload reloads the file from disk.
void TryApplyVariantScale(XDocument variantFile)
bool Save(string fileNameWithoutExtension=null)
List< ColliderParams > Colliders
List< LimbParams > Limbs
CanEnterSubmarine CanEnterSubmarine
List< JointParams > Joints
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive=false)
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180