using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; using LimbParams = Barotrauma.RagdollParams.LimbParams; using JointParams = Barotrauma.RagdollParams.JointParams; namespace Barotrauma { abstract partial class Ragdoll { public abstract RagdollParams RagdollParams { get; protected set; } const float ImpactDamageMultiplayer = 10.0f; /// /// Maximum damage per impact (0.1 = 10% of the character's maximum health) /// const float MaxImpactDamage = 0.1f; private static readonly List list = new List(); struct Impact { public Fixture F1, F2; public Vector2 LocalNormal; public Vector2 Velocity; public Vector2 ImpactPos; public Impact(Fixture f1, Fixture f2, Contact contact, Vector2 velocity) { F1 = f1; F2 = f2; Velocity = velocity; LocalNormal = contact.Manifold.LocalNormal; contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2 points); ImpactPos = points[0]; } } private readonly Queue impactQueue = new Queue(); protected Hull currentHull; private bool accessRemovedCharacterErrorShown; private Limb[] limbs; public Limb[] Limbs { get { if (limbs == null) { LogAccessedRemovedCharacterError(); return Array.Empty(); } return limbs; } } public bool HasMultipleLimbsOfSameType => limbs != null && limbs.Length > limbDictionary.Count; private bool frozen; public bool Frozen { get { return frozen; } set { if (frozen == value) return; frozen = value; Collider.FarseerBody.LinearDamping = frozen ? (1.5f / (float)Timing.Step) : 0.0f; Collider.FarseerBody.AngularDamping = frozen ? (1.5f / (float)Timing.Step) : PhysicsBody.DefaultAngularDamping; Collider.FarseerBody.IgnoreGravity = frozen; //Collider.PhysEnabled = !frozen; if (frozen && MainLimb != null) { MainLimb.PullJointWorldAnchorB = MainLimb.SimPosition; } } } private Dictionary limbDictionary; public LimbJoint[] LimbJoints; private bool simplePhysicsEnabled; public Character Character => character; protected Character character; protected float strongestImpact; private float splashSoundTimer; //the ragdoll builds a "tolerance" to the flow force when being pushed by water. //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. private float flowForceTolerance, flowStunTolerance; //the movement speed of the ragdoll public Vector2 movement; //the target speed towards which movement is interpolated protected Vector2 targetMovement; //a movement vector that overrides targetmovement if trying to steer //a Character to the position sent by server in multiplayer mode protected Vector2 overrideTargetMovement; protected float floorY, standOnFloorY; protected Fixture floorFixture; protected Vector2 floorNormal = Vector2.UnitY; protected float surfaceY; protected bool inWater, headInWater; protected bool onGround; public bool OnGround => onGround; private Vector2 lastFloorCheckPos; private bool lastFloorCheckIgnoreStairs, lastFloorCheckIgnorePlatforms; /// /// In sim units. Joint scale applied. /// public float ColliderHeightFromFloor => ConvertUnits.ToSimUnits(RagdollParams.ColliderHeightFromFloor) * RagdollParams.JointScale; public Structure Stairs; protected Direction dir; public Direction TargetDir; protected List collider; protected int colliderIndex = 0; private Category prevCollisionCategory = Category.None; public bool IsStuck => Limbs.Any(l => l.IsStuck); public PhysicsBody Collider { get { return collider?[colliderIndex]; } } public bool TryGetCollider(int index, out PhysicsBody collider) { collider = null; try { collider = this.collider?[index]; return true; } catch { return false; } } public int ColliderIndex { get { return colliderIndex; } set { if (value == colliderIndex || collider == null) { return; } if (value >= collider.Count || value < 0) { return; } if (collider[colliderIndex].Height < collider[value].Height) { Vector2 pos1 = collider[colliderIndex].SimPosition; pos1.Y -= collider[colliderIndex].Height * ColliderHeightFromFloor; Vector2 pos2 = pos1; pos2.Y += collider[value].Height * 1.1f; if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall) && !(f.Body.UserData is Submarine))) { return; } } Vector2 pos = collider[colliderIndex].SimPosition; pos.Y -= collider[colliderIndex].Height * 0.5f; pos.Y += collider[value].Height * 0.5f; collider[value].SetTransform(pos, collider[colliderIndex].Rotation); collider[value].LinearVelocity = collider[colliderIndex].LinearVelocity; collider[value].AngularVelocity = collider[colliderIndex].AngularVelocity; collider[value].Submarine = collider[colliderIndex].Submarine; collider[value].PhysEnabled = !frozen; collider[value].Enabled = !simplePhysicsEnabled; collider[colliderIndex].PhysEnabled = false; colliderIndex = value; } } public float FloorY { get { return floorY; } } public float Mass { get; private set; } public void SubtractMass(Limb limb) { if (limbs.Contains(limb)) { Mass -= limb.Mass; } } public Limb MainLimb { get { Limb mainLimb = GetLimb(RagdollParams.MainLimb); if (!IsValid(mainLimb)) { Limb torso = GetLimb(LimbType.Torso); Limb head = GetLimb(LimbType.Head); mainLimb = torso ?? head; if (!IsValid(mainLimb)) { mainLimb = Limbs.FirstOrDefault(l => IsValid(l)); } if (mainLimb == null) { DebugConsole.ThrowError("Couldn't find a valid main limb. The limb can't be hidden nor be set to ignore collisions!"); mainLimb = Limbs.FirstOrDefault(); } } static bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.IgnoreCollisions && !limb.Hidden; return mainLimb; } } public Vector2 WorldPosition { get { return character.Submarine == null ? ConvertUnits.ToDisplayUnits(Collider.SimPosition) : ConvertUnits.ToDisplayUnits(Collider.SimPosition) + character.Submarine.Position; } } public bool SimplePhysicsEnabled { get { return simplePhysicsEnabled; } set { if (value == simplePhysicsEnabled) { return; } simplePhysicsEnabled = value; foreach (Limb limb in Limbs) { if (limb.IsSevered) { continue; } if (limb.body == null) { DebugConsole.ThrowError("Limb has no body! (" + (character != null ? character.Name : "Unknown character") + ", " + limb.type.ToString()); continue; } limb.body.Enabled = !simplePhysicsEnabled; } foreach (LimbJoint joint in LimbJoints) { joint.Enabled = !joint.IsSevered && !simplePhysicsEnabled; } if (!simplePhysicsEnabled) { foreach (Limb limb in Limbs) { if (limb.IsSevered || !limb.body.PhysEnabled) { continue; } limb.body.SetTransform(Collider.SimPosition, Collider.Rotation); //reset pull joints (they may be somewhere far away if the character has moved from the position where animations were last updated) limb.PullJointEnabled = false; limb.PullJointWorldAnchorB = limb.SimPosition; } } } } public const float MAX_SPEED = 20; public Vector2 TargetMovement { get { return (overrideTargetMovement == Vector2.Zero) ? targetMovement : overrideTargetMovement; } set { if (!MathUtils.IsValid(value)) return; targetMovement.X = MathHelper.Clamp(value.X, -MAX_SPEED, MAX_SPEED); targetMovement.Y = MathHelper.Clamp(value.Y, -MAX_SPEED, MAX_SPEED); } } public abstract float? HeadPosition { get; } public abstract float? HeadAngle { get; } public abstract float? TorsoPosition { get; } public abstract float? TorsoAngle { get; } float? impactTolerance; public float ImpactTolerance { get { if (impactTolerance == null) { impactTolerance = RagdollParams.ImpactTolerance; if (character.Params.VariantFile != null) { float? tolerance = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("ragdoll")?.GetAttributeFloat("impacttolerance", impactTolerance.Value); if (tolerance.HasValue) { impactTolerance = tolerance; } } } return impactTolerance.Value; } } public bool Draggable => RagdollParams.Draggable; public CanEnterSubmarine CanEnterSubmarine => RagdollParams.CanEnterSubmarine; public float Dir => dir == Direction.Left ? -1.0f : 1.0f; public Direction Direction => dir; public bool InWater { get { return inWater; } } public bool HeadInWater { get { return headInWater; } } public Hull CurrentHull { get { return currentHull; } set { if (value == currentHull) return; currentHull = value; Submarine currSubmarine = currentHull?.Submarine; foreach (Limb limb in Limbs) { if (limb.IsSevered) { continue; } limb.body.Submarine = currSubmarine; } Collider.Submarine = currSubmarine; } } public bool IgnorePlatforms { get; set; } /// /// Call this to create the ragdoll from the RagdollParams. /// public virtual void Recreate(RagdollParams ragdollParams = null) { if (IsFlipped) { Flip(); } dir = Direction.Right; Dictionary> items = null; if (ragdollParams != null) { RagdollParams = ragdollParams; if (!character.VariantOf.IsEmpty) { RagdollParams.TryApplyVariantScale(character.Params.VariantFile); } } else { items = limbs?.ToDictionary(l => l.Params, l => l.WearingItems); } foreach (var limbParams in RagdollParams.Limbs) { if (!PhysicsBody.IsValidShape(limbParams.Radius, limbParams.Height, limbParams.Width)) { DebugConsole.ThrowError($"Invalid collider dimensions (r: {limbParams.Radius}, h: {limbParams.Height}, w: {limbParams.Width}) on limb: {limbParams.Name}. Fixing."); limbParams.Radius = 10; } } foreach (var colliderParams in RagdollParams.Colliders) { if (!PhysicsBody.IsValidShape(colliderParams.Radius, colliderParams.Height, colliderParams.Width)) { DebugConsole.ThrowError($"Invalid collider dimensions (r: {colliderParams.Radius}, h: {colliderParams.Height}, w: {colliderParams.Width}) on collider: {colliderParams.Name}. Fixing."); colliderParams.Radius = 10; } } CreateColliders(); CreateLimbs(); CreateJoints(); UpdateCollisionCategories(); character.LoadHeadAttachments(); if (items != null) { foreach (var kvp in items) { int id = kvp.Key.ID; // This can be the case if we manipulate the ragdoll at runtime (husk appendage, limb removal in the character editor) if (id > limbs.Length - 1) { continue; } var limb = limbs[id]; var itemList = kvp.Value; limb.WearingItems.AddRange(itemList); } } if (character.IsHusk && character.Params.UseHuskAppendage) { bool inEditor = false; #if CLIENT inEditor = Screen.Selected == GameMain.CharacterEditorScreen; #endif var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath); if (characterPrefab?.ConfigElement != null) { var mainElement = characterPrefab.ConfigElement; foreach (var huskAppendage in mainElement.GetChildElements("huskappendage")) { if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; } Identifier afflictionIdentifier = huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty); if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab affliction) || affliction is not AfflictionPrefabHusk matchingAffliction) { DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!", contentPackage: huskAppendage.ContentPackage); } else { AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskAppendage, ragdoll: this); } } } } } public Ragdoll(Character character, string seed, RagdollParams ragdollParams = null) { list.Add(this); this.character = character; Recreate(ragdollParams ?? RagdollParams); } protected void CreateColliders() { collider?.ForEach(c => c.Remove()); DebugConsole.Log($"Creating colliders from {RagdollParams.Name}."); collider = new List(); foreach (var cParams in RagdollParams.Colliders) { if (!PhysicsBody.IsValidShape(cParams.Radius, cParams.Height, cParams.Width)) { DebugConsole.ThrowError("Invalid collider dimensions: " + cParams.Name); break; ; } var body = new PhysicsBody(cParams); collider.Add(body); body.UserData = character; body.FarseerBody.OnCollision += OnLimbCollision; if (collider.Count > 1) { body.PhysEnabled = false; } } } protected void CreateJoints() { if (LimbJoints != null) { foreach (LimbJoint joint in LimbJoints) { if (GameMain.World.JointList.Contains(joint.Joint)) { GameMain.World.Remove(joint.Joint); } } } DebugConsole.Log($"Creating joints from {RagdollParams.Name}."); LimbJoints = new LimbJoint[RagdollParams.Joints.Count]; RagdollParams.Joints.ForEach(j => AddJoint(j)); // Check the joints for (int i = 0; i < LimbJoints.Length; i++) { if (LimbJoints[i] == null) { DebugConsole.ThrowError($"Joint {i} null."); } } UpdateCollisionCategories(); SetInitialLimbPositions(); } private void SetInitialLimbPositions() { foreach (var joint in LimbJoints) { if (joint == null) { continue; } float angle = (joint.LowerLimit + joint.UpperLimit) / 2.0f; joint.LimbB?.body?.SetTransform( (joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, joint.BodyA.Rotation + angle, true)), joint.BodyA.Rotation + angle); } } protected void CreateLimbs() { limbs?.ForEach(l => l.Remove()); DebugConsole.Log($"Creating limbs from {RagdollParams.Name}."); limbDictionary = new Dictionary(); limbs = new Limb[RagdollParams.Limbs.Count]; RagdollParams.Limbs.ForEach(l => AddLimb(l)); if (limbs.Contains(null)) { return; } SetupDrawOrder(); } partial void SetupDrawOrder(); /// /// Saves all serializable data in the currently selected ragdoll params. This method should properly handle character flipping. /// public void SaveRagdoll(string fileNameWithoutExtension = null) { RagdollParams.Save(fileNameWithoutExtension); } /// /// Resets the serializable data to the currently selected ragdoll params. /// Force reloading always loads the xml stored on the disk. /// public void ResetRagdoll(bool forceReload = false) { RagdollParams.Reset(forceReload); ResetJoints(); ResetLimbs(); } /// /// Resets the current joint values to the serialized joint params. /// public void ResetJoints() { LimbJoints.ForEach(j => j.LoadParams()); } /// /// Resets the current limb values to the serialized limb params. /// public void ResetLimbs() { Limbs.ForEach(l => l.LoadParams()); SetupDrawOrder(); } public void AddJoint(JointParams jointParams) { if (!checkLimbIndex(jointParams.Limb2, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2")) { return; } LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this); GameMain.World.Add(joint.Joint); for (int i = 0; i < LimbJoints.Length; i++) { if (LimbJoints[i] != null) continue; LimbJoints[i] = joint; return; } Array.Resize(ref LimbJoints, LimbJoints.Length + 1); LimbJoints[LimbJoints.Length - 1] = joint; bool checkLimbIndex(int index, string debugName) { if (index < 0 || index >= limbs.Length) { string errorMsg = $"Failed to add a joint to character {character.Name}. {debugName} out of bounds (index: {index}, limbs: {limbs.Length}."; DebugConsole.ThrowError(errorMsg, contentPackage: jointParams.Element?.ContentPackage); if (jointParams.Element?.ContentPackage == GameMain.VanillaContent) { GameAnalyticsManager.AddErrorEventOnce("Ragdoll.AddJoint:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } return false; } return true; } } protected void AddLimb(LimbParams limbParams) { if (limbParams.ID < 0 || limbParams.ID > 255) { throw new Exception($"Invalid limb params in limb \"{limbParams.Type}\". \"{limbParams.ID}\" is not a valid limb ID."); } byte ID = Convert.ToByte(limbParams.ID); Limb limb = new Limb(this, character, limbParams); limb.body.FarseerBody.OnCollision += OnLimbCollision; if (ID >= Limbs.Length) { 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."); } Limbs[ID] = limb; Mass += limb.Mass; if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); } } public void AddLimb(Limb limb) { if (Limbs.Contains(limb)) { return; } limb.body.FarseerBody.OnCollision += OnLimbCollision; Array.Resize(ref limbs, Limbs.Length + 1); Limbs[Limbs.Length - 1] = limb; Mass += limb.Mass; if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); } SetupDrawOrder(); } public void RemoveLimb(Limb limb) { if (!Limbs.Contains(limb)) return; Limb[] newLimbs = new Limb[Limbs.Length - 1]; int i = 0; foreach (Limb existingLimb in Limbs) { if (existingLimb == limb) continue; newLimbs[i] = existingLimb; i++; } limbs = newLimbs; if (limbDictionary.ContainsKey(limb.type)) { limbDictionary.Remove(limb.type); // If there is another limb of the same type, replace the limb in the dictionary. if (HasMultipleLimbsOfSameType) { var otherLimb = Limbs.FirstOrDefault(l => l != limb && l.type == limb.type); if (otherLimb != null) { limbDictionary.Add(otherLimb.type, otherLimb); } } } // TODO: this could be optimized if needed, but at least we need to remove the limb from the inversedDrawOrder array. SetupDrawOrder(); //remove all joints that were attached to the removed limb LimbJoint[] attachedJoints = Array.FindAll(LimbJoints, lj => lj.LimbA == limb || lj.LimbB == limb); if (attachedJoints.Length > 0) { LimbJoint[] newJoints = new LimbJoint[LimbJoints.Length - attachedJoints.Length]; i = 0; foreach (LimbJoint limbJoint in LimbJoints) { if (attachedJoints.Contains(limbJoint)) continue; newJoints[i] = limbJoint; i++; } LimbJoints = newJoints; } SubtractMass(limb); limb.Remove(); foreach (LimbJoint limbJoint in attachedJoints) { GameMain.World.Remove(limbJoint.Joint); } } private enum LimbStairCollisionResponse { DontClimbStairs, ClimbWithoutLimbCollision, ClimbWithLimbCollision } public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact) { if (f2.Body.UserData is Submarine submarine && character.Submarine == submarine) { return false; } if (f2.UserData is Hull) { if (character.Submarine != null) { return false; } if (CanEnterSubmarine == CanEnterSubmarine.Partial) { //collider collides with hulls to prevent the character going fully inside the sub, limbs don't return f1.Body == Collider.FarseerBody || (f1.Body.UserData is Limb limb && !limb.Params.CanEnterSubmarine); } } //using the velocity of the limb would make the impact damage more realistic, //but would also make it harder to edit the animations because the forces/torques //would all have to be balanced in a way that prevents the character from doing //impact damage to itself Vector2 velocity = Collider.LinearVelocity; if (character.Submarine == null && f2.Body.UserData is Submarine sub) { velocity -= sub.Velocity; } //always collides with bodies other than structures if (f2.Body.UserData is not Structure structure) { if (!f2.IsSensor) { lock (impactQueue) { impactQueue.Enqueue(new Impact(f1, f2, contact, velocity)); } } return true; } else if (character.Submarine != null && structure.Submarine != null && character.Submarine != structure.Submarine) { return false; } Vector2 colliderBottom = GetColliderBottom(); if (structure.IsPlatform) { if (IgnorePlatforms || currentHull == null) { return false; } if (colliderBottom.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; } if (f1.Body.Position.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; } } else if (structure.StairDirection != Direction.None) { if (character.SelectedBy != null) { Stairs = character.SelectedBy.AnimController.Stairs; } var collisionResponse = getStairCollisionResponse(); if (collisionResponse == LimbStairCollisionResponse.ClimbWithLimbCollision) { Stairs = structure; } else { if (collisionResponse == LimbStairCollisionResponse.DontClimbStairs) { Stairs = null; } return false; } LimbStairCollisionResponse getStairCollisionResponse() { //don't collide with stairs if //1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10); if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; } if (character.SelectedBy != null && character.SelectedBy.AnimController.GetColliderBottom().Y < stairBottomPos && character.SelectedBy.AnimController.targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; } //2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return LimbStairCollisionResponse.DontClimbStairs; } //3. collided with the stairs from below if (contact.Manifold.LocalNormal.Y < 0.0f) { return Stairs != structure ? LimbStairCollisionResponse.DontClimbStairs : LimbStairCollisionResponse.ClimbWithoutLimbCollision; } //4. contact points is above the bottom half of the collider contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2 points); if (points[0].Y > Collider.SimPosition.Y) { return LimbStairCollisionResponse.DontClimbStairs; } //5. in water if (inWater && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; } return LimbStairCollisionResponse.ClimbWithLimbCollision; } } lock (impactQueue) { impactQueue.Enqueue(new Impact(f1, f2, contact, velocity)); } return true; } private void ApplyImpact(Fixture f1, Fixture f2, Vector2 localNormal, Vector2 impactPos, Vector2 velocity) { if (character.DisableImpactDamageTimer > 0.0f) { return; } if (f2.Body?.UserData is Item) { //no impact damage from items //items that can impact characters (melee weapons, projectiles) should handle the damage themselves return; } Vector2 normal = localNormal; float impact = Vector2.Dot(velocity, -normal); if (f1.Body == Collider.FarseerBody || !Collider.Enabled) { bool isNotRemote = true; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { isNotRemote = !character.IsRemotelyControlled; } if (isNotRemote) { float impactTolerance = ImpactTolerance; if (character.Stun > 0.0f) { impactTolerance *= 0.5f; } if (impact > impactTolerance) { impactPos = ConvertUnits.ToDisplayUnits(impactPos); if (character.Submarine != null) { impactPos += character.Submarine.Position; } float impactDamage = GetImpactDamage(impact, impactTolerance); character.LastDamageSource = null; character.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), 0.0f, true); strongestImpact = Math.Max(strongestImpact, impact - impactTolerance); character.ApplyStatusEffects(ActionType.OnImpact, 1.0f); //briefly disable impact damage //otherwise the character will take damage multiple times when for example falling, //because we use the velocity of the collider to determine the impact //(i.e. the character would take damage until the collider hits the floor and stops) character.DisableImpactDamageTimer = 0.25f; } } } ImpactProjSpecific(impact, f1.Body); } public float GetImpactDamage(float impact, float? impactTolerance = null) { float tolerance = impactTolerance ?? ImpactTolerance; return Math.Min((impact - tolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage); } private readonly List connectedLimbs = new List(); private readonly List checkedJoints = new List(); public bool SeverLimbJoint(LimbJoint limbJoint) { if (!limbJoint.CanBeSevered || limbJoint.IsSevered) { return false; } limbJoint.IsSevered = true; limbJoint.Enabled = false; Vector2 limbDiff = limbJoint.LimbA.SimPosition - limbJoint.LimbB.SimPosition; if (limbDiff.LengthSquared() < 0.0001f) { limbDiff = Rand.Vector(1.0f); } limbDiff = Vector2.Normalize(limbDiff); float mass = limbJoint.BodyA.Mass + limbJoint.BodyB.Mass; limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * Math.Min(mass, limbJoint.BodyA.Mass * 500), (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * Math.Min(mass, limbJoint.BodyB.Mass * 500), (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); connectedLimbs.Clear(); checkedJoints.Clear(); GetConnectedLimbs(connectedLimbs, checkedJoints, MainLimb); foreach (Limb limb in Limbs) { if (connectedLimbs.Contains(limb)) { continue; } limb.IsSevered = true; if (limb.type == LimbType.RightHand) { character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.Drop(character); } else if (limb.type == LimbType.LeftHand) { character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.Drop(character); } } if (!string.IsNullOrEmpty(character.BloodDecalName)) { character.CurrentHull?.AddDecal(character.BloodDecalName, (limbJoint.LimbA.WorldPosition + limbJoint.LimbB.WorldPosition) / 2, MathHelper.Clamp(Math.Min(limbJoint.LimbA.Mass, limbJoint.LimbB.Mass), 0.5f, 2.0f), isNetworkEvent: false); } SeverLimbJointProjSpecific(limbJoint, playSound: true); if (GameMain.NetworkMember is { IsServer: true }) { GameMain.NetworkMember.CreateEntityEvent(character, new Character.CharacterStatusEventData()); } return true; } partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound); protected List GetConnectedLimbs(Limb limb) { connectedLimbs.Clear(); checkedJoints.Clear(); GetConnectedLimbs(connectedLimbs, checkedJoints, limb); return connectedLimbs; } private void GetConnectedLimbs(List connectedLimbs, List checkedJoints, Limb limb) { connectedLimbs.Add(limb); foreach (LimbJoint joint in LimbJoints) { if (joint.IsSevered || checkedJoints.Contains(joint)) { continue; } if (joint.LimbA == limb) { if (!connectedLimbs.Contains(joint.LimbB)) { checkedJoints.Add(joint); GetConnectedLimbs(connectedLimbs, checkedJoints, joint.LimbB); } } else if (joint.LimbB == limb) { if (!connectedLimbs.Contains(joint.LimbA)) { checkedJoints.Add(joint); GetConnectedLimbs(connectedLimbs, checkedJoints, joint.LimbA); } } } } partial void ImpactProjSpecific(float impact, Body body); public bool IsFlipped { get; private set; } public virtual void Flip() { IsFlipped = !IsFlipped; dir = (dir == Direction.Left) ? Direction.Right : Direction.Left; for (int i = 0; i < LimbJoints.Length; i++) { float lowerLimit = -LimbJoints[i].UpperLimit; float upperLimit = -LimbJoints[i].LowerLimit; LimbJoints[i].LowerLimit = lowerLimit; LimbJoints[i].UpperLimit = upperLimit; LimbJoints[i].LocalAnchorA = new Vector2(-LimbJoints[i].LocalAnchorA.X, LimbJoints[i].LocalAnchorA.Y); LimbJoints[i].LocalAnchorB = new Vector2(-LimbJoints[i].LocalAnchorB.X, LimbJoints[i].LocalAnchorB.Y); } foreach (Limb limb in Limbs) { if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; } limb.Dir = Dir; limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y); limb.MirrorPullJoint(); } FlipProjSpecific(); } partial void FlipProjSpecific(); public Vector2 GetCenterOfMass() { //all limbs disabled -> use the position of the collider if (!Limbs.Any(l => !l.IsSevered && l.body.Enabled)) { return Collider.SimPosition; } Vector2 centerOfMass = Vector2.Zero; float totalMass = 0.0f; foreach (Limb limb in Limbs) { if (limb.IsSevered || !limb.body.Enabled) continue; centerOfMass += limb.Mass * limb.SimPosition; totalMass += limb.Mass; } if (totalMass <= 0.0f) return Collider.SimPosition; centerOfMass /= totalMass; if (!MathUtils.IsValid(centerOfMass)) { string errorMsg = "Ragdoll.GetCenterOfMass returned an invalid value (" + centerOfMass + "). Limb positions: {" + string.Join(", ", limbs.Select(l => l.SimPosition)) + "}, total mass: " + totalMass + "."; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return Collider.SimPosition; } return centerOfMass; } /// if false, force is applied to the position of pullJoint public void MoveLimb(Limb limb, Vector2 pos, float amount, bool pullFromCenter = false) { limb.MoveToPos(pos, amount, pullFromCenter); } public void ResetPullJoints(Func condition = null) { for (int i = 0; i < Limbs.Length; i++) { if (Limbs[i] == null) { continue; } if (condition != null && !condition(Limbs[i])) { continue; } Limbs[i].PullJointEnabled = false; } } public static void UpdateAll(float deltaTime, Camera cam) { foreach (Ragdoll r in list) { r.UpdateRagdoll(deltaTime, cam); } } public void FindHull(Vector2? worldPosition = null, bool setSubmarine = true) { Vector2 findPos = worldPosition == null ? this.WorldPosition : (Vector2)worldPosition; if (!MathUtils.IsValid(findPos)) { GameAnalyticsManager.AddErrorEventOnce( "Ragdoll.FindHull:InvalidPosition", GameAnalyticsManager.ErrorSeverity.Error, "Attempted to find a hull at an invalid position (" + findPos + ")\n" + Environment.StackTrace.CleanupStackTrace()); return; } Hull newHull = Hull.FindHull(findPos, currentHull); if (newHull == currentHull) { return; } if (CanEnterSubmarine == CanEnterSubmarine.False || (character.AIController != null && character.AIController.CanEnterSubmarine == CanEnterSubmarine.False)) { //character is inside the sub even though it shouldn't be able to enter -> teleport it out //far from an ideal solution, but monsters getting lodged inside the sub seems to be //pretty rare during normal gameplay (requires abnormally high velocities), so I think //this is preferable to the cost of using continuous collision detection for the character collider if (newHull?.Submarine != null) { Vector2 hullDiff = WorldPosition - newHull.WorldPosition; Vector2 moveDir = hullDiff.LengthSquared() < 0.001f ? Vector2.UnitY : Vector2.Normalize(hullDiff); //find a position 32 units away from the hull if (MathUtils.GetLineRectangleIntersection( newHull.WorldPosition, newHull.WorldPosition + moveDir * Math.Max(newHull.Rect.Width, newHull.Rect.Height), new Rectangle(newHull.WorldRect.X - 32, newHull.WorldRect.Y + 32, newHull.WorldRect.Width + 64, newHull.Rect.Height + 64), out Vector2 intersection)) { Collider.SetTransform(ConvertUnits.ToSimUnits(intersection), Collider.Rotation); } return; } } if (CanEnterSubmarine != CanEnterSubmarine.True) { return; } if (setSubmarine) { //in -> out if (newHull?.Submarine == null && currentHull?.Submarine != null) { //don't teleport out yet if the character is going through a gap if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f, allowRoomToRoom: true) != null) { return; } if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) { return; } character.MemLocalState?.Clear(); Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity); } //out -> in else if (currentHull == null && newHull.Submarine != null) { character.MemLocalState?.Clear(); Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity); } //from one sub to another else if (newHull != null && currentHull != null && newHull.Submarine != currentHull.Submarine) { character.MemLocalState?.Clear(); Vector2 newSubPos = newHull.Submarine == null ? Vector2.Zero : newHull.Submarine.Position; Vector2 prevSubPos = currentHull.Submarine == null ? Vector2.Zero : currentHull.Submarine.Position; Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero); } } CurrentHull = newHull; character.Submarine = currentHull?.Submarine; foreach (var attachedProjectile in character.AttachedProjectiles) { attachedProjectile.Item.CurrentHull = currentHull; attachedProjectile.Item.Submarine = character.Submarine; attachedProjectile.Item.UpdateTransform(); } } private void PreventOutsideCollision() { if (currentHull?.Submarine == null) { return; } var connectedGaps = currentHull.ConnectedGaps.Where(g => !g.IsRoomToRoom); foreach (Gap gap in connectedGaps) { if (gap.IsHorizontal) { if (character.Position.Y > gap.Rect.Y || character.Position.Y < gap.Rect.Y - gap.Rect.Height) continue; if (Math.Sign(gap.Rect.Center.X - currentHull.Rect.Center.X) != Math.Sign(character.Position.X - currentHull.Rect.Center.X)) { continue; } } else { if (character.Position.X < gap.Rect.X || character.Position.X > gap.Rect.Right) continue; if (Math.Sign((gap.Rect.Y - gap.Rect.Height / 2) - (currentHull.Rect.Y - currentHull.Rect.Height / 2)) != Math.Sign(character.Position.Y - (currentHull.Rect.Y - currentHull.Rect.Height / 2))) { continue; } } gap.RefreshOutsideCollider(); } } public void Teleport(Vector2 moveAmount, Vector2 velocityChange, bool detachProjectiles = true) { foreach (Limb limb in Limbs) { if (limb.IsSevered) { continue; } if (limb.body.FarseerBody.ContactList == null) { continue; } ContactEdge ce = limb.body.FarseerBody.ContactList; while (ce != null && ce.Contact != null) { ce.Contact.Enabled = false; ce = ce.Next; } } foreach (Limb limb in Limbs) { if (limb.IsSevered) { continue; } limb.body.LinearVelocity += velocityChange; } character.DisableImpactDamageTimer = 0.25f; SetPosition(Collider.SimPosition + moveAmount); character.CursorPosition += moveAmount; Collider?.UpdateDrawPosition(); foreach (Limb limb in Limbs) { limb.body.UpdateDrawPosition(); } } private void UpdateCollisionCategories() { Category wall = currentHull?.Submarine == null ? Physics.CollisionLevel | Physics.CollisionWall : Physics.CollisionWall; Category collisionCategory = (IgnorePlatforms) ? wall | Physics.CollisionProjectile | Physics.CollisionStairs : wall | Physics.CollisionProjectile | Physics.CollisionPlatform | Physics.CollisionStairs; if (collisionCategory == prevCollisionCategory) { return; } prevCollisionCategory = collisionCategory; Collider.CollidesWith = collisionCategory | Physics.CollisionItemBlocking; foreach (Limb limb in Limbs) { if (limb.IgnoreCollisions || limb.IsSevered) { continue; } try { limb.body.CollidesWith = collisionCategory; } catch (Exception e) { DebugConsole.ThrowError("Failed to update ragdoll limb collisioncategories", e); } } } protected bool levitatingCollider = true; /// /// How long has the ragdoll stayed motionless /// private float bodyInRestTimer; private float BodyInRestDelay = 1.0f; public bool BodyInRest { get { return bodyInRestTimer > BodyInRestDelay; } set { foreach (Limb limb in Limbs) { limb.body.PhysEnabled = !value; } bodyInRestTimer = value ? BodyInRestDelay : 0.0f; } } public bool forceStanding; public bool forceNotStanding; public void UpdateRagdoll(float deltaTime, Camera cam) { if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; } while (impactQueue.Count > 0) { var impact = impactQueue.Dequeue(); ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity); } CheckValidity(); UpdateNetPlayerPosition(deltaTime); CheckDistFromCollider(); UpdateCollisionCategories(); FindHull(); PreventOutsideCollision(); CheckBodyInRest(deltaTime); splashSoundTimer -= deltaTime; if (character.Submarine == null && Level.Loaded != null) { if (Collider.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y) { Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, Math.Min(Collider.LinearVelocity.Y, -1)); } else if (Collider.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y) { Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, MathHelper.Clamp(Collider.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - Collider.SimPosition.Y, 10.0f)); } foreach (Limb limb in Limbs) { if (limb.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y) { limb.body.LinearVelocity = new Vector2(limb.LinearVelocity.X, Math.Min(limb.LinearVelocity.Y, -1)); } else if (limb.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y) { limb.body.LinearVelocity = new Vector2( limb.LinearVelocity.X, MathHelper.Clamp(limb.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - limb.SimPosition.Y, 10.0f)); } } } if (forceStanding) { inWater = false; headInWater = false; RefreshFloorY(deltaTime, ignoreStairs: Stairs == null); } //ragdoll isn't in any room -> it's in the water else if (currentHull == null) { inWater = true; headInWater = true; } else { headInWater = false; inWater = false; RefreshFloorY(deltaTime, ignoreStairs: Stairs == null); if (currentHull.WaterPercentage > 0.001f) { (float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY(); float waterSurfaceY = ConvertUnits.ToSimUnits(waterSurfaceDisplayUnits); float ceilingY = ConvertUnits.ToSimUnits(ceilingDisplayUnits); if (targetMovement.Y < 0.0f) { Vector2 colliderBottom = GetColliderBottom(); floorY = Math.Min(colliderBottom.Y, floorY); //check if the bottom of the collider is below the current hull if (floorY < ConvertUnits.ToSimUnits(currentHull.Rect.Y - currentHull.Rect.Height)) { //set floorY to the position of the floor in the hull below the character var lowerHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(colliderBottom), useWorldCoordinates: false); if (lowerHull != null) { floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); } } } float standHeight = HeadPosition ?? TorsoPosition ?? Collider.GetMaxExtent() * 0.5f; if (Collider.SimPosition.Y < waterSurfaceY) { //too deep to stand up, or not enough room to stand up if (waterSurfaceY - floorY > standHeight * 0.8f || ceilingY - floorY < standHeight * 0.8f) { inWater = true; } } } } UpdateHullFlowForces(deltaTime); if (currentHull == null || currentHull.WaterVolume > currentHull.Volume * 0.95f || ConvertUnits.ToSimUnits(currentHull.Surface) > Collider.SimPosition.Y) { Collider.ApplyWaterForces(); } foreach (Limb limb in Limbs) { //find the room which the limb is in //the room where the ragdoll is in is used as the "guess", meaning that it's checked first Hull newHull = currentHull == null ? null : Hull.FindHull(limb.WorldPosition, currentHull); bool prevInWater = limb.InWater; limb.InWater = false; if (forceStanding) { limb.InWater = false; } else if (newHull == null) { //limb isn't in any room -> it's in the water limb.InWater = true; if (limb.type == LimbType.Head) { headInWater = true; } } else if (newHull.WaterVolume > 0.0f && Submarine.RectContains(newHull.Rect, limb.Position)) { if (limb.Position.Y < newHull.Surface) { limb.InWater = true; surfaceY = newHull.Surface; if (limb.type == LimbType.Head) { headInWater = true; } } //the limb has gone through the surface of the water if (Math.Abs(limb.LinearVelocity.Y) > 5.0f && limb.InWater != prevInWater && newHull == limb.Hull) { Splash(limb, newHull); //if the Character dropped into water, create a wave if (limb.LinearVelocity.Y < 0.0f) { Vector2 impulse = limb.LinearVelocity * limb.Mass; int n = (int)((limb.Position.X - newHull.Rect.X) / Hull.WaveWidth); newHull.WaveVel[n] += MathHelper.Clamp(impulse.Y, -5.0f, 5.0f); } } } limb.Hull = newHull; limb.Update(deltaTime); } if (!inWater && character.AllowInput && levitatingCollider) { if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance) { float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor; const float LevitationSpeedMultiplier = 5f; // If the character is walking down a slope, target a position that moves along it float slopePull = 0f; if (floorNormal.Y is > 0f and < 1f && Math.Sign(movement.X) == Math.Sign(floorNormal.X)) { float steepness = Math.Abs(floorNormal.X); slopePull = Math.Abs(movement.X * steepness) / LevitationSpeedMultiplier; } if (Math.Abs(Collider.SimPosition.Y - targetY - slopePull) > 0.01f) { float yVelocity = (targetY - Collider.SimPosition.Y) * LevitationSpeedMultiplier; if (Stairs != null && targetY < Collider.SimPosition.Y) { yVelocity = Math.Sign(yVelocity); } yVelocity -= slopePull * LevitationSpeedMultiplier; Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, yVelocity); } } else { // Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck. if (Collider.LinearVelocity == Vector2.Zero) { character.IsRagdolled = true; if (character.IsBot) { // Seems to work without this on player controlled characters -> not sure if we should call it always or just for the bots. character.SetInput(InputType.Ragdoll, hit: false, held: true); } } } } UpdateProjSpecific(deltaTime, cam); forceNotStanding = false; } private void CheckBodyInRest(float deltaTime) { if (SimplePhysicsEnabled) { return; } if (InWater || Collider.LinearVelocity.LengthSquared() > 0.01f || character.SelectedBy != null || !character.IsDead) { bodyInRestTimer = 0.0f; foreach (Limb limb in Limbs) { limb.body.PhysEnabled = true; } } else if (Limbs.All(l => l != null && !l.body.Enabled || l.LinearVelocity.LengthSquared() < 0.001f)) { bodyInRestTimer += deltaTime; if (bodyInRestTimer > BodyInRestDelay) { foreach (Limb limb in Limbs) { limb.body.PhysEnabled = false; } } } } public bool Invalid { get; private set; } private int validityResets; private bool CheckValidity() { if (limbs == null) { 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)); Invalid = true; return false; } bool isColliderValid = CheckValidity(Collider); if (!isColliderValid) { Collider.ResetDynamics(); } bool limbsValid = true; foreach (Limb limb in limbs) { if (limb?.body == null || !limb.body.Enabled) { continue; } if (!CheckValidity(limb.body)) { limbsValid = false; limb.body.ResetDynamics(); break; } } bool isValid = isColliderValid && limbsValid; if (!isValid) { validityResets++; if (validityResets > 3) { Invalid = true; DebugConsole.ThrowError("Invalid ragdoll physics. Ragdoll frozen to prevent crashes."); Collider.SetTransform(Vector2.Zero, 0.0f); Collider.ResetDynamics(); foreach (Limb limb in Limbs) { limb.body?.SetTransform(Collider.SimPosition, 0.0f); limb.body?.ResetDynamics(); } Frozen = true; } } return isValid; } private bool CheckValidity(PhysicsBody body) { string errorMsg = null; if (!MathUtils.IsValid(body.SimPosition) || Math.Abs(body.SimPosition.X) > 1e10f || Math.Abs(body.SimPosition.Y) > 1e10f) { errorMsg = GetBodyName() + " position invalid (" + body.SimPosition + ", character: [name])."; } else if (!MathUtils.IsValid(body.LinearVelocity) || Math.Abs(body.LinearVelocity.X) > 1000f || Math.Abs(body.LinearVelocity.Y) > 1000f) { errorMsg = GetBodyName() + " velocity invalid (" + body.LinearVelocity + ", character: [name])."; } else if (!MathUtils.IsValid(body.Rotation)) { errorMsg = GetBodyName() + " rotation invalid (" + body.Rotation + ", character: [name])."; } else if (!MathUtils.IsValid(body.AngularVelocity) || Math.Abs(body.AngularVelocity) > 1000f) { errorMsg = GetBodyName() + " angular velocity invalid (" + body.AngularVelocity + ", character: [name])."; } if (errorMsg != null) { if (character.IsRemotelyControlled) { errorMsg += " Ragdoll controlled remotely."; } if (SimplePhysicsEnabled) { errorMsg += " Simple physics enabled."; } if (GameMain.NetworkMember != null) { errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server."; } #if DEBUG DebugConsole.ThrowError(errorMsg.Replace("[name]", Character.Name)); #else DebugConsole.NewMessage(errorMsg.Replace("[name]", Character.Name), Color.Red); #endif GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", Character.SpeciesName.Value)); if (!MathUtils.IsValid(Collider.SimPosition) || Math.Abs(Collider.SimPosition.X) > 1e10f || Math.Abs(Collider.SimPosition.Y) > 1e10f) { Collider.SetTransform(Vector2.Zero, 0.0f); } foreach (Limb otherLimb in Limbs) { otherLimb.body.SetTransform(Collider.SimPosition, 0.0f); otherLimb.body.ResetDynamics(); } SetInitialLimbPositions(); return false; } string GetBodyName() { return body.UserData is Limb limb ? "Limb (" + limb.type + ")" : "Collider"; } return true; } protected void LogAccessedRemovedCharacterError() { if (!accessRemovedCharacterErrorShown) { string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this); errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "Ragdoll:AccessRemoved", GameAnalyticsManager.ErrorSeverity.Error, "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()); accessRemovedCharacterErrorShown = true; } } partial void UpdateProjSpecific(float deltaTime, Camera cam); partial void Splash(Limb limb, Hull limbHull); private void UpdateHullFlowForces(float deltaTime) { if (currentHull == null) { return; } const float StunForceThreshold = 5.0f; const float StunDuration = 0.5f; const float ToleranceIncreaseSpeed = 5.0f; const float ToleranceDecreaseSpeed = 1.0f; //how much distance to a gap affects the force it exerts on the character const float DistanceFactor = 0.5f; const float ForceMultiplier = 0.035f; Vector2 flowForce = Vector2.Zero; foreach (Gap gap in Gap.GapList) { if (gap.Open <= 0.0f || !gap.linkedTo.Contains(currentHull) || gap.LerpedFlowForce.LengthSquared() < 0.01f) { continue; } float dist = Vector2.Distance(MainLimb.WorldPosition, gap.WorldPosition) * DistanceFactor; flowForce += Vector2.Normalize(gap.LerpedFlowForce) * (Math.Max(gap.LerpedFlowForce.Length() - dist, 0.0f) * ForceMultiplier); } //throwing conscious/moving characters around takes more force -> double the flow force if (character.CanMove) { flowForce *= 2.0f; } flowForce *= 1 - Math.Clamp(character.GetStatValue(StatTypes.FlowResistance), 0f, 1f); float flowForceMagnitude = flowForce.Length(); float limbMultipier = limbs.Count(l => l.InWater) / (float)limbs.Length; //if the force strong enough, stun the character to let it get thrown around by the water if ((flowForceMagnitude * limbMultipier) - flowStunTolerance > StunForceThreshold) { character.Stun = Math.Max(character.Stun, StunDuration); flowStunTolerance = Math.Max(flowStunTolerance, flowForceMagnitude); } if (character == Character.Controlled && inWater && Screen.Selected?.Cam != null) { float shakeStrength = Math.Min(flowForceMagnitude / 10.0f, 5.0f) * limbMultipier; Screen.Selected.Cam.Shake = Math.Max(Screen.Selected.Cam.Shake, shakeStrength); } if (flowForceMagnitude > 0.0001f) { flowForce = Vector2.Normalize(flowForce) * Math.Max(flowForceMagnitude - flowForceTolerance, 0.0f); } if (flowForceTolerance <= flowForceMagnitude * 1.5f && inWater) { //build up "tolerance" to the flow force //ensures the character won't get permanently stuck by forces, while allowing sudden changes in flow to push the character hard flowForceTolerance += deltaTime * ToleranceIncreaseSpeed; flowStunTolerance = Math.Max(flowStunTolerance, flowForceTolerance); } else { flowForceTolerance = Math.Max(flowForceTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f); flowStunTolerance = Math.Max(flowStunTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f); } if (flowForce.LengthSquared() > 0.001f) { Collider.ApplyForce(flowForce * (Collider.Mass / Mass)); foreach (Limb limb in limbs) { if (!limb.InWater) { continue; } limb.body.ApplyForce(flowForce * (limb.Mass / Mass * limbs.Length)); } } } public void ForceRefreshFloorY() { lastFloorCheckPos = Vector2.Zero; } // Force check floor y at least once a second so that we'll drop through gaps that we are standing upon. private const float FloorYStaleTime = 1; private float floorYCheckTimer; private void RefreshFloorY(float deltaTime, bool ignoreStairs = false) { floorYCheckTimer -= deltaTime; PhysicsBody refBody = Collider; if (floorYCheckTimer < 0 || lastFloorCheckIgnoreStairs != ignoreStairs || lastFloorCheckIgnorePlatforms != IgnorePlatforms || Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f) { floorY = GetFloorY(refBody.SimPosition, ignoreStairs); lastFloorCheckPos = refBody.SimPosition; lastFloorCheckIgnoreStairs = ignoreStairs; lastFloorCheckIgnorePlatforms = IgnorePlatforms; // Add some randomness to prevent all stationary characters doing the checks at the same frame. floorYCheckTimer = FloorYStaleTime * Rand.Range(0.9f, 1.1f); } } private float GetFloorY(Vector2 simPosition, bool ignoreStairs = false) { onGround = false; Stairs = null; floorFixture = null; Vector2 rayStart = simPosition; float height = ColliderHeightFromFloor; if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); } if (TorsoPosition.HasValue && MathUtils.IsValid(TorsoPosition.Value)) { height = Math.Max(height, TorsoPosition.Value); } Vector2 rayEnd = rayStart - new Vector2(0.0f, height * 2f); Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom()); Fixture standOnFloorFixture = null; float standOnFloorFraction = 1; float closestFraction = 1; GameMain.World.RayCast((fixture, point, normal, fraction) => { switch (fixture.CollisionCategories) { case Physics.CollisionStairs: if (inWater && TargetMovement.Y < 0.5f) { return -1; } if (character.SelectedBy == null && fraction < standOnFloorFraction) { Structure structure = fixture.Body.UserData as Structure; if (colliderBottomDisplay.Y >= structure.Rect.Y - structure.Rect.Height + 30 || TargetMovement.Y > 0.5f || Stairs != null) { standOnFloorFraction = fraction; standOnFloorFixture = fixture; } } if (ignoreStairs) { return -1; } break; case Physics.CollisionPlatform: Structure platform = fixture.Body.UserData as Structure; if (!IgnorePlatforms && fraction < standOnFloorFraction) { if (colliderBottomDisplay.Y >= platform.Rect.Y - 16 || (targetMovement.Y > 0.0f && Stairs == null)) { standOnFloorFraction = fraction; standOnFloorFixture = fixture; } } if (colliderBottomDisplay.Y < platform.Rect.Y - 16 && (targetMovement.Y <= 0.0f || Stairs != null)) return -1; if (IgnorePlatforms && TargetMovement.Y < -0.5f || Collider.Position.Y < platform.Rect.Y) return -1; break; case Physics.CollisionWall: case Physics.CollisionLevel: if (!fixture.CollidesWith.HasFlag(Physics.CollisionCharacter)) { return -1; } if (fixture.Body.UserData is Submarine && character.Submarine != null) { return -1; } if (fixture.IsSensor) { return -1; } if (fraction < standOnFloorFraction) { standOnFloorFraction = fraction; standOnFloorFixture = fixture; } break; default: System.Diagnostics.Debug.Assert(false, "Floor raycast should not have hit a fixture with the collision category " + fixture.CollisionCategories); return -1; } if (fraction < closestFraction) { floorNormal = normal; closestFraction = fraction; } return closestFraction; }, rayStart, rayEnd, Physics.CollisionStairs | Physics.CollisionPlatform | Physics.CollisionWall | Physics.CollisionLevel); if (standOnFloorFixture != null && !IsHangingWithRope) { floorFixture = standOnFloorFixture; standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction; if (rayStart.Y - standOnFloorY < Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor * 1.2f) { onGround = true; if (standOnFloorFixture.CollisionCategories == Physics.CollisionStairs) { Stairs = standOnFloorFixture.Body.UserData as Structure; } } } if (closestFraction >= 1) //raycast didn't hit anything { floorNormal = Vector2.UnitY; if (CurrentHull == null) { return -1000.0f; } else { float hullBottom = currentHull.Rect.Y - currentHull.Rect.Height; //check if there's a connected hull below foreach (var gap in currentHull.ConnectedGaps) { if (!gap.IsRoomToRoom || gap.Open < 1.0f || gap.ConnectedDoor != null || gap.IsHorizontal) { continue; } if (WorldPosition.X > gap.WorldRect.X && WorldPosition.X < gap.WorldRect.Right && gap.WorldPosition.Y < WorldPosition.Y) { var lowerHull = gap.linkedTo[0] == currentHull ? gap.linkedTo[1] : gap.linkedTo[0]; hullBottom = Math.Min(hullBottom, lowerHull.Rect.Y - lowerHull.Rect.Height); } } return ConvertUnits.ToSimUnits(hullBottom); } } else { return rayStart.Y + (rayEnd.Y - rayStart.Y) * closestFraction; } } /// /// Get the position of the surface of water at the position of the character, in display units (taking into account connected hulls above the hull the character is in) /// public float GetSurfaceY() { return GetWaterSurfaceAndCeilingY().WaterSurfaceY; } /// /// Get the position of the surface of water and the ceiling (= upper edge of the hull) at the position of the character, in display units (taking into account connected hulls above the hull the character is in). /// private (float WaterSurfaceY, float CeilingY) GetWaterSurfaceAndCeilingY() { //check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside if (currentHull == null || character.CurrentHull == null) { return (float.PositiveInfinity, float.PositiveInfinity); } float surfaceY = currentHull.Surface; float ceilingY = currentHull.Rect.Y; float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); //if the hull is almost full of water, check if there's a water-filled hull above it //and use its water surface instead of the current hull's if (currentHull.Rect.Y - currentHull.Surface < 5.0f) { GetSurfacePos(currentHull, ref surfaceY, ref ceilingY); void GetSurfacePos(Hull hull, ref float prevSurfacePos, ref float ceilingPos) { if (prevSurfacePos > surfaceThreshold) { return; } foreach (Gap gap in hull.ConnectedGaps) { if (gap.IsHorizontal || gap.Open <= 0.0f || gap.WorldPosition.Y < hull.WorldPosition.Y) { continue; } if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) { continue; } //if the gap is above us and leads outside, there's no surface to limit the movement if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y) { ceilingPos += 100000.0f; prevSurfacePos += 100000.0f; return; } foreach (var linkedTo in gap.linkedTo) { if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull) { prevSurfacePos = Math.Max(surfaceY, otherHull.Surface); ceilingPos = Math.Max(ceilingPos, otherHull.Rect.Y); GetSurfacePos(otherHull, ref prevSurfacePos, ref ceilingPos); break; } } } } } return (surfaceY, ceilingY); } public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool moveLatchers = true) { if (!MathUtils.IsValid(simPosition)) { DebugConsole.ThrowError("Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace()); GameAnalyticsManager.AddErrorEventOnce( "Ragdoll.SetPosition:InvalidPosition", GameAnalyticsManager.ErrorSeverity.Error, "Attempted to move a ragdoll (" + character.SpeciesName + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace()); return; } if (MainLimb == null) { return; } Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition; // A Work-around for an issue with teleporting the characters: // Detach every latcher when either one of the latchers or the target is teleported, // because otherwise all the characters are teleported to invalid positions. const float ForceDeattachThreshold = 10.0f; if (limbMoveAmount.LengthSquared() > ForceDeattachThreshold * ForceDeattachThreshold && Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached) { var target = enemyAI.LatchOntoAI.TargetCharacter; if (target != null) { target.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true)); target.Latchers.Clear(); } enemyAI.LatchOntoAI.DeattachFromBody(reset: true); } Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true)); Character.Latchers.Clear(); if (lerp) { Collider.TargetPosition = simPosition; Collider.MoveToTargetPosition(true); } else { Collider.SetTransform(simPosition, Collider.Rotation); } if (!MathUtils.NearlyEqual(limbMoveAmount, Vector2.Zero)) { foreach (Limb limb in Limbs) { if (limb.IsSevered) { continue; } //check visibility from the new position of the collider to the new position of this limb Vector2 movePos = limb.SimPosition + limbMoveAmount; TrySetLimbPosition(limb, simPosition, movePos, limb.Rotation, lerp, ignorePlatforms); } } } /// /// Is attached to something with a rope. /// public bool IsHoldingToRope { get; private set; } protected bool shouldHoldToRope; /// /// Is hanging to something with a rope, so that can reel towards it. Currently only possible in water. /// public bool IsHangingWithRope { get; private set; } protected bool shouldHangWithRope; /// /// Has someone attached to the character with a rope? /// public bool IsDraggedWithRope { get; private set; } protected bool shouldBeDraggedWithRope; public void HangWithRope() { shouldHangWithRope = true; IsHangingWithRope = true; ResetPullJoints(); onGround = false; levitatingCollider = false; } public void HoldToRope() { shouldHoldToRope = true; IsHoldingToRope = true; } public void DragWithRope() { shouldBeDraggedWithRope = true; IsDraggedWithRope = true; } protected void StopHangingWithRope() { shouldHangWithRope = false; IsHangingWithRope = false; } protected void StopHoldingToRope() { shouldHoldToRope = false; IsHoldingToRope = false; } protected void StopGettingDraggedWithRope() { shouldBeDraggedWithRope = false; IsDraggedWithRope = false; } protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true) { Vector2 movePos = simPosition; Vector2 prevPosition = limb.body.SimPosition; if (Vector2.DistanceSquared(original, simPosition) > 0.0001f) { Category collisionCategory = Physics.CollisionWall | Physics.CollisionLevel; if (!ignorePlatforms) { collisionCategory |= Physics.CollisionPlatform; } Body body = Submarine.PickBody(original, simPosition, null, collisionCategory); //if there's something in between the limbs if (body != null) { //move the limb close to the position where the raycast hit something movePos = original + ((simPosition - original) * Submarine.LastPickedFraction * 0.9f); } } if (lerp) { limb.body.TargetPosition = movePos; limb.body.TargetRotation = rotation; limb.body.MoveToTargetPosition(true); } else { limb.body.SetTransform(movePos, rotation); limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA; limb.PullJointEnabled = false; } foreach (var attachedProjectile in character.AttachedProjectiles) { if (attachedProjectile.IsAttachedTo(limb.body)) { attachedProjectile.Item.SetTransform( attachedProjectile.Item.SimPosition + (movePos - prevPosition), attachedProjectile.Item.body.Rotation, findNewHull: false); } } } private bool collisionsDisabled; private double lastObstacleRayCastTime; protected void CheckDistFromCollider() { float allowedDist = Math.Max(Math.Max(Collider.Radius, Collider.Width), Collider.Height) * 2.0f; allowedDist = Math.Max(allowedDist, 1.0f); float resetDist = allowedDist * 5.0f; float obstacleCheckDist = 0.3f; Vector2 diff = Collider.SimPosition - MainLimb.SimPosition; float distSqrd = diff.LengthSquared(); bool shouldReset = distSqrd > resetDist * resetDist; if (!shouldReset && distSqrd > obstacleCheckDist * obstacleCheckDist) { if (Timing.TotalTime > lastObstacleRayCastTime + 1 && Submarine.PickBody(Collider.SimPosition, MainLimb.SimPosition, collisionCategory: Physics.CollisionWall) != null) { shouldReset = true; lastObstacleRayCastTime = Timing.TotalTime; } } if (shouldReset) { //ragdoll way too far, reset position SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true); } else if (distSqrd > allowedDist * allowedDist) { //ragdoll too far from the collider, disable collisions until it's close enough //(in case the ragdoll has gotten stuck somewhere) Vector2 forceDir = diff / (float)Math.Sqrt(distSqrd); foreach (Limb limb in Limbs) { if (limb.IsSevered) { continue; } limb.body.CollidesWith = Physics.CollisionNone; limb.body.ApplyForce(forceDir * limb.Mass * 10.0f, maxVelocity: 10.0f); } collisionsDisabled = true; } else if (collisionsDisabled) { //set the position of the ragdoll to make sure limbs don't get stuck inside walls when re-enabling collisions SetPosition(Collider.SimPosition, lerp: true); collisionsDisabled = false; //force collision categories to be updated prevCollisionCategory = Category.None; } } partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos); private void UpdateNetPlayerPosition(float deltaTime) { if (GameMain.NetworkMember == null) return; float lowestSubPos = float.MaxValue; if (Submarine.Loaded.Any()) { lowestSubPos = ConvertUnits.ToSimUnits(Submarine.Loaded.Min(s => s.HiddenSubPosition.Y - s.Borders.Height - 128.0f)); for (int i = 0; i < character.MemState.Count; i++) { if (character.Submarine == null) { //transform in-sub coordinates to outside coordinates if (character.MemState[i].Position.Y > lowestSubPos) character.MemState[i].TransformInToOutside(); } else if (currentHull?.Submarine != null) { //transform outside coordinates to in-sub coordinates if (character.MemState[i].Position.Y < lowestSubPos) character.MemState[i].TransformOutToInside(currentHull.Submarine); } } } UpdateNetPlayerPositionProjSpecific(deltaTime, lowestSubPos); } /// /// Note that if there are multiple limbs of the same type, only the first (valid) limb is returned. /// public Limb GetLimb(LimbType limbType, bool excludeSevered = true) { if (limbDictionary.TryGetValue(limbType, out Limb limb)) { if (excludeSevered && limb.IsSevered) { limb = null; } } if (limb == null && HasMultipleLimbsOfSameType) { // Didn't find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs. foreach (var l in limbs) { if (l.type != limbType) { continue; } if (!excludeSevered || !l.IsSevered) { limb = l; break; } } } return limb; } public Vector2? GetMouthPosition() { Limb mouthLimb = GetLimb(LimbType.Head); if (mouthLimb == null) { return null; } float cos = (float)Math.Cos(mouthLimb.Rotation); float sin = (float)Math.Sin(mouthLimb.Rotation); Vector2 bodySize = mouthLimb.body.GetSize(); Vector2 offset = new Vector2(mouthLimb.MouthPos.X * bodySize.X / 2, mouthLimb.MouthPos.Y * bodySize.Y / 2); return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos); } public Vector2 GetColliderBottom() { float offset = 0.0f; if (!character.IsDead && character.Stun <= 0.0f && !character.IsIncapacitated) { offset = -ColliderHeightFromFloor; } float lowestBound = Collider.SimPosition.Y; if (Collider.FarseerBody.FixtureList != null) { for (int i = 0; i < Collider.FarseerBody.FixtureList.Count; i++) { Collider.FarseerBody.GetTransform(out FarseerPhysics.Common.Transform transform); Collider.FarseerBody.FixtureList[i].Shape.ComputeAABB(out FarseerPhysics.Collision.AABB aabb, ref transform, i); lowestBound = Math.Min(aabb.LowerBound.Y, lowestBound); } } return new Vector2(Collider.SimPosition.X, lowestBound + offset); } public Limb FindLowestLimb() { Limb lowestLimb = null; foreach (Limb limb in Limbs) { if (limb.IsSevered) { continue; } if (lowestLimb == null) { lowestLimb = limb; } else if (limb.SimPosition.Y < lowestLimb.SimPosition.Y) { lowestLimb = limb; } } return lowestLimb; } public void ReleaseStuckLimbs() { // 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. //Limbs.ForEach(l => l.Release()); } public void HideAndDisable(LimbType limbType, float duration = 0, bool ignoreCollisions = true) { foreach (var limb in Limbs) { if (limb.type == limbType) { limb.HideAndDisable(duration, ignoreCollisions); } } } public void RestoreTemporarilyDisabled() => Limbs.ForEach(l => l.ReEnable()); public void Remove() { if (Limbs != null) { foreach (Limb l in Limbs) { l?.Remove(); } limbs = null; } if (collider != null) { foreach (PhysicsBody b in collider) { b?.Remove(); } collider = null; } if (LimbJoints != null) { foreach (var joint in LimbJoints) { var j = joint?.Joint; if (GameMain.World.JointList.Contains(j)) { GameMain.World.Remove(j); } } LimbJoints = null; } list.Remove(this); } public static void RemoveAll() { for (int i = list.Count - 1; i >= 0; i--) { list[i].Remove(); } System.Diagnostics.Debug.Assert(list.Count == 0, "Some ragdolls were not removed in Ragdoll.RemoveAll"); } } }