using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Factories; using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Xml.Linq; namespace Barotrauma { class PosInfo { public Vector2 Position { get; private set; } public float? Rotation { get; private set; } public Vector2 LinearVelocity { get; private set; } public float? AngularVelocity { get; private set; } public readonly float Timestamp; public readonly UInt16 ID; public PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, float time) : this(pos, rotation, linearVelocity, angularVelocity, 0, time) { } public PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, UInt16 ID) : this(pos, rotation, linearVelocity, angularVelocity, ID, 0.0f) { } protected PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, UInt16 ID, float time) { Position = pos; Rotation = rotation; LinearVelocity = linearVelocity; AngularVelocity = angularVelocity; this.ID = ID; Timestamp = time; } public void TransformOutToInside(Submarine submarine) { //transform outside coordinates to in-sub coordinates Position -= ConvertUnits.ToSimUnits(submarine.Position); } public void TransformInToOutside() { var sub = Submarine.FindContaining(ConvertUnits.ToDisplayUnits(Position)); if (sub != null) { Position += ConvertUnits.ToSimUnits(sub.Position); } } public void Translate(Vector2 posAmount,float rotationAmount) { Position += posAmount; Rotation += rotationAmount; } } partial class PhysicsBody { public enum Shape { Circle, Rectangle, Capsule, HorizontalCapsule }; private static List list = new List(); public static List List { get { return list; } } //the farseer physics body of the item private Body body; protected Vector2 prevPosition; protected float prevRotation; protected Vector2? targetPosition; protected float? targetRotation; private Vector2 drawPosition; private float drawRotation; public Vector2 LastSentPosition { get; private set; } private Shape bodyShape; public float height, width, radius; private float density; //the direction the item is facing (for example, a gun has to be //flipped horizontally if the Character holding it turns around) float dir = 1.0f; private Vector2 drawOffset; private float rotationOffset; private float lastProcessedNetworkState; public float? PositionSmoothingFactor; public Shape BodyShape { get { return bodyShape; } } public Vector2? TargetPosition { get { return targetPosition; } set { if (value == null) { targetPosition = null; } else { if (!IsValidValue(value.Value, "target position", -1e5f, 1e5f)) return; targetPosition = new Vector2( MathHelper.Clamp(((Vector2)value).X, -10000.0f, 10000.0f), MathHelper.Clamp(((Vector2)value).Y, -10000.0f, 10000.0f)); } } } public float? TargetRotation { get { return targetRotation; } set { if (value == null) { targetRotation = null; } else { if (!IsValidValue(value.Value, "target rotation")) return; targetRotation = value; } } } public Vector2 DrawPosition { get { return Submarine == null ? drawPosition : drawPosition + Submarine.DrawPosition; } } public float DrawRotation { get { return drawRotation; } } public Submarine Submarine; public float Dir { get { return dir; } set { dir = value; } } private bool isEnabled = true; private bool isPhysEnabled = true; public bool Enabled { get { return isEnabled; } set { isEnabled = value; try { if (isEnabled) body.Enabled = isPhysEnabled; else body.Enabled = false; } catch (Exception e) { DebugConsole.ThrowError("Exception in PhysicsBody.Enabled = " + value + " (" + isPhysEnabled + ")", e); if (UserData != null) DebugConsole.NewMessage("PhysicsBody UserData: " + UserData.GetType().ToString(), Color.Red); if (GameMain.World.ContactManager == null) DebugConsole.NewMessage("ContactManager is null!", Color.Red); else if (GameMain.World.ContactManager.BroadPhase == null) DebugConsole.NewMessage("Broadphase is null!", Color.Red); if (body.FixtureList == null) DebugConsole.NewMessage("FixtureList is null!", Color.Red); if (UserData is Entity entity) { DebugConsole.NewMessage("Entity \"" + entity.ToString() + "\" removed!", Color.Red); } } } } public bool PhysEnabled { get { return body.Enabled; } set { isPhysEnabled = value; if (Enabled) body.Enabled = value; } } public Vector2 SimPosition { get { return body.Position; } } public Vector2 Position { get { return ConvertUnits.ToDisplayUnits(body.Position); } } public Vector2 PrevPosition { get { return prevPosition; } } public float Rotation { get { return body.Rotation; } } /// /// Takes flipping (Dir) into account. /// public float TransformedRotation => Dir < 0 ? Rotation - MathHelper.Pi : Rotation; public Vector2 LinearVelocity { get { return body.LinearVelocity; } set { if (!IsValidValue(value, "velocity", -1000.0f, 1000.0f)) return; body.LinearVelocity = value; } } public float AngularVelocity { get { return body.AngularVelocity; } set { if (!IsValidValue(value, "angular velocity", -1000f, 1000f)) return; body.AngularVelocity = value; } } public float Mass { get { return body.Mass; } } public float Density { get { return density; } } public Body FarseerBody { get { return body; } } public object UserData { get { return body.UserData; } set { body.UserData = value; } } public float Friction { set { body.Friction = value; } } public BodyType BodyType { get { return body.BodyType; } set { body.BodyType = value; } } public Category CollisionCategories { set { body.CollisionCategories = value; } } public Category CollidesWith { set { body.CollidesWith = value; } } public PhysicsBody(XElement element, float scale = 1.0f) : this(element, Vector2.Zero, scale) { } public PhysicsBody(ColliderParams cParams) : this(cParams, Vector2.Zero) { } public PhysicsBody(LimbParams lParams) : this(lParams, Vector2.Zero) { } public PhysicsBody(float width, float height, float radius, float density) { CreateBody(width, height, radius, density); LastSentPosition = body.Position; list.Add(this); } public PhysicsBody(Body farseerBody) { body = farseerBody; if (body.UserData == null) body.UserData = this; LastSentPosition = body.Position; list.Add(this); } public PhysicsBody(ColliderParams colliderParams, Vector2 position) { float radius = ConvertUnits.ToSimUnits(colliderParams.Radius) * colliderParams.Ragdoll.LimbScale; float height = ConvertUnits.ToSimUnits(colliderParams.Height) * colliderParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(colliderParams.Width) * colliderParams.Ragdoll.LimbScale; density = 10; CreateBody(width, height, radius, density); body.BodyType = BodyType.Dynamic; body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; body.CollisionCategories = Physics.CollisionCharacter; body.AngularDamping = 5.0f; body.FixedRotation = true; body.Friction = 0.05f; body.Restitution = 0.05f; SetTransformIgnoreContacts(position, 0.0f); LastSentPosition = position; list.Add(this); } public PhysicsBody(LimbParams limbParams, Vector2 position) { float radius = ConvertUnits.ToSimUnits(limbParams.Radius) * limbParams.Ragdoll.LimbScale; float height = ConvertUnits.ToSimUnits(limbParams.Height) * limbParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(limbParams.Width) * limbParams.Ragdoll.LimbScale; density = limbParams.Density; CreateBody(width, height, radius, density); body.BodyType = BodyType.Dynamic; body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; body.CollisionCategories = Physics.CollisionItem; body.Friction = limbParams.Friction; body.Restitution = limbParams.Restitution; body.UserData = this; SetTransformIgnoreContacts(position, 0.0f); LastSentPosition = position; list.Add(this); } public PhysicsBody(XElement element, Vector2 position, float scale=1.0f) { float radius = ConvertUnits.ToSimUnits(element.GetAttributeFloat("radius", 0.0f)) * scale; float height = ConvertUnits.ToSimUnits(element.GetAttributeFloat("height", 0.0f)) * scale; float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale; density = element.GetAttributeFloat("density", 10.0f); CreateBody(width, height, radius, density); //Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); body.BodyType = BodyType.Dynamic; body.CollisionCategories = Physics.CollisionItem; body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; body.Friction = element.GetAttributeFloat("friction", 0.3f); body.Restitution = element.GetAttributeFloat("restitution", 0.05f); body.UserData = this; SetTransformIgnoreContacts(position, 0.0f); LastSentPosition = position; list.Add(this); } private void CreateBody(float width, float height, float radius, float density) { if (IsValidShape(radius, height, width)) { bodyShape = DefineBodyShape(radius, width, height); switch (bodyShape) { case Shape.Capsule: body = BodyFactory.CreateCapsule(GameMain.World, height, radius, density); break; case Shape.HorizontalCapsule: body = BodyFactory.CreateCapsuleHorizontal(GameMain.World, width, radius, density); break; case Shape.Circle: body = BodyFactory.CreateCircle(GameMain.World, radius, density); break; case Shape.Rectangle: body = BodyFactory.CreateRectangle(GameMain.World, width, height, density); break; default: throw new NotImplementedException(bodyShape.ToString()); } } else { DebugConsole.ThrowError("Invalid physics body dimensions (width: " + width + ", height: " + height + ", radius: " + radius + ")"); } this.width = width; this.height = height; this.radius = radius; } /// /// Returns the farthest point towards the forward of the body. /// For capsules and circles, the front is at the top. /// For horizontal capsules, the front is at the right-most point. /// For rectangles, the front is either at the top or at the right, depending on which one of the two is greater: width or height. /// The rotation is in radians. /// public Vector2 GetLocalFront(float spritesheetRotation = 0) { Vector2 pos; switch (bodyShape) { case Shape.Capsule: pos = new Vector2(0.0f, height / 2 + radius); break; case Shape.HorizontalCapsule: pos = new Vector2(width / 2 + radius, 0.0f); break; case Shape.Circle: pos = new Vector2(0.0f, radius); break; case Shape.Rectangle: pos = new Vector2(0.0f, Math.Max(height, width) / 2.0f); break; default: throw new NotImplementedException(); } return spritesheetRotation == 0 ? pos : Vector2.Transform(pos, Matrix.CreateRotationZ(spritesheetRotation)); } public float GetMaxExtent() { switch (bodyShape) { case Shape.Capsule: return height / 2 + radius; case Shape.HorizontalCapsule: return width / 2 + radius; case Shape.Circle: return radius; case Shape.Rectangle: return new Vector2(width * 0.5f, height * 0.5f).Length(); default: throw new NotImplementedException(); } } public Vector2 GetSize() { switch (bodyShape) { case Shape.Capsule: return new Vector2(radius * 2, height + radius * 2); case Shape.HorizontalCapsule: return new Vector2(width + radius * 2, radius * 2); case Shape.Circle: return new Vector2(radius * 2); case Shape.Rectangle: return new Vector2(width, height); default: throw new NotImplementedException(); } } public void SetSize(Vector2 size) { switch (bodyShape) { case Shape.Capsule: radius = Math.Max(size.X / 2, 0); height = Math.Max(size.Y - size.X, 0); width = 0; break; case Shape.HorizontalCapsule: radius = Math.Max(size.Y / 2, 0); width = Math.Max(size.X - size.Y, 0); height = 0; break; case Shape.Circle: radius = Math.Max(Math.Min(size.X, size.Y) / 2, 0); width = 0; height = 0; break; case Shape.Rectangle: width = Math.Max(size.X, 0); height = Math.Max(size.Y, 0); radius = 0; break; default: throw new NotImplementedException(); } #if CLIENT bodyShapeTexture = null; #endif } public bool IsValidValue(float value, string valueName, float? minValue = null, float? maxValue = null) { if (!MathUtils.IsValid(value) || (minValue.HasValue && value < minValue.Value) || (maxValue.HasValue && value > maxValue.Value)) { string userData = UserData == null ? "null" : UserData.ToString(); string errorMsg = "Attempted to apply invalid " + valueName + " to a physics body (userdata: " + userData + "), value: " + value; if (GameMain.NetworkMember != null) { errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server."; } errorMsg += "\n" + Environment.StackTrace; if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "PhysicsBody.SetPosition:InvalidPosition" + userData, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return false; } return true; } private bool IsValidValue(Vector2 value, string valueName, float? minValue = null, float? maxValue = null) { if (!MathUtils.IsValid(value) || (minValue.HasValue && (value.X < minValue.Value || value.Y < minValue.Value)) || (maxValue.HasValue && (value.X > maxValue.Value || value.Y > maxValue))) { string userData = UserData == null ? "null" : UserData.ToString(); string errorMsg = "Attempted to apply invalid " + valueName + " to a physics body (userdata: " + userData + "), value: " + value; if (GameMain.NetworkMember != null) { errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server."; } errorMsg += "\n" + Environment.StackTrace; if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "PhysicsBody.SetPosition:InvalidPosition" + userData, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return false; } return true; } public void ResetDynamics() { body.ResetDynamics(); } public void ApplyLinearImpulse(Vector2 impulse) { if (!IsValidValue(impulse / body.Mass, "new velocity", -1000f, 1000f)) return; if (!IsValidValue(impulse, "impulse", -1e10f, 1e10f)) return; body.ApplyLinearImpulse(impulse); } /// /// Apply an impulse to the body without increasing it's velocity above a specific limit. /// public void ApplyLinearImpulse(Vector2 impulse, float maxVelocity) { if (!IsValidValue(impulse / body.Mass, "new velocity")) return; if (!IsValidValue(impulse, "impulse", -1e10f, 1e10f)) return; if (!IsValidValue(maxVelocity, "max velocity")) return; float currSpeed = body.LinearVelocity.Length(); Vector2 velocityAddition = impulse / Mass; Vector2 newVelocity = body.LinearVelocity + velocityAddition; newVelocity = newVelocity.ClampLength(Math.Max(currSpeed, maxVelocity)); body.ApplyLinearImpulse((newVelocity - body.LinearVelocity) * Mass); } public void ApplyLinearImpulse(Vector2 impulse, Vector2 point) { if (!IsValidValue(impulse, "impulse", -1e10f, 1e10f)) return; if (!IsValidValue(point, "point")) return; if (!IsValidValue(impulse / body.Mass, "new velocity")) return; body.ApplyLinearImpulse(impulse, point); } public void ApplyForce(Vector2 force) { if (!IsValidValue(force, "force", -1e10f, 1e10f)) return; body.ApplyForce(force); } /// /// Apply an impulse to the body without increasing it's velocity above a specific limit. /// public void ApplyForce(Vector2 force, float maxVelocity) { if (!IsValidValue(force, "force", -1e10f, 1e10f)) return; if (!IsValidValue(maxVelocity, "max velocity")) return; float currSpeed = body.LinearVelocity.Length(); Vector2 velocityAddition = force / Mass * (float)Timing.Step; Vector2 newVelocity = body.LinearVelocity + velocityAddition; newVelocity = newVelocity.ClampLength(Math.Max(currSpeed, maxVelocity)); body.ApplyForce((newVelocity - body.LinearVelocity) * Mass / (float)Timing.Step); } public void ApplyForce(Vector2 force, Vector2 point) { if (!IsValidValue(force, "force", -1e10f, 1e10f)) return; if (!IsValidValue(point, "point")) return; body.ApplyForce(force, point); } public void ApplyTorque(float torque) { if (!IsValidValue(torque, "torque")) return; body.ApplyTorque(torque); } public bool SetTransform(Vector2 simPosition, float rotation) { System.Diagnostics.Debug.Assert(MathUtils.IsValid(simPosition)); System.Diagnostics.Debug.Assert(Math.Abs(simPosition.X) < 1000000.0f); System.Diagnostics.Debug.Assert(Math.Abs(simPosition.Y) < 1000000.0f); if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) return false; if (!IsValidValue(rotation, "rotation")) return false; body.SetTransform(simPosition, rotation); SetPrevTransform(simPosition, rotation); return true; } public bool SetTransformIgnoreContacts(Vector2 simPosition, float rotation) { System.Diagnostics.Debug.Assert(MathUtils.IsValid(simPosition)); System.Diagnostics.Debug.Assert(Math.Abs(simPosition.X) < 1000000.0f); System.Diagnostics.Debug.Assert(Math.Abs(simPosition.Y) < 1000000.0f); if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) return false; if (!IsValidValue(rotation, "rotation")) return false; body.SetTransformIgnoreContacts(ref simPosition, rotation); SetPrevTransform(simPosition, rotation); return true; } public void SetPrevTransform(Vector2 simPosition, float rotation) { if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) return; if (!IsValidValue(rotation, "rotation")) return; prevPosition = simPosition; prevRotation = rotation; } public void MoveToTargetPosition(bool lerp = true) { if (targetPosition == null) { return; } if (lerp) { if (Vector2.DistanceSquared((Vector2)targetPosition, body.Position) < 10.0f * 10.0f) { drawOffset = -((Vector2)targetPosition - (body.Position + drawOffset)); prevPosition = (Vector2)targetPosition; } else { drawOffset = Vector2.Zero; } if (targetRotation.HasValue) { rotationOffset = -MathUtils.GetShortestAngle(body.Rotation + rotationOffset, targetRotation.Value); } } SetTransform((Vector2)targetPosition, targetRotation == null ? body.Rotation : (float)targetRotation); targetPosition = null; targetRotation = null; } public void MoveToPos(Vector2 simPosition, float force, Vector2? pullPos = null) { if (pullPos == null) pullPos = body.Position; if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) return; if (!IsValidValue(force, "force")) return; Vector2 vel = body.LinearVelocity; Vector2 deltaPos = simPosition - (Vector2)pullPos; deltaPos *= force; body.ApplyLinearImpulse((deltaPos - vel * 0.5f) * body.Mass, (Vector2)pullPos); } /// /// Applies buoyancy, drag and angular drag caused by water /// public void ApplyWaterForces() { //buoyancy Vector2 buoyancy = new Vector2(0, Mass * 9.6f); Vector2 dragForce = Vector2.Zero; if (LinearVelocity.LengthSquared() > 0.00001f) { //drag Vector2 velDir = Vector2.Normalize(LinearVelocity); float vel = LinearVelocity.Length() * 2.0f; float drag = vel * vel * Math.Max(height + radius * 2, height); dragForce = Math.Min(drag, Mass * 500.0f) * -velDir; } ApplyForce(dragForce + buoyancy); ApplyTorque(body.AngularVelocity * body.Mass * -0.08f); } public void Update(float deltaTime) { if (drawOffset.LengthSquared() < 0.01f) { PositionSmoothingFactor = null; } drawOffset = NetConfig.InterpolateSimPositionError(drawOffset, PositionSmoothingFactor); rotationOffset = NetConfig.InterpolateRotationError(rotationOffset); } public void UpdateDrawPosition() { drawPosition = Timing.Interpolate(prevPosition, body.Position); drawPosition = ConvertUnits.ToDisplayUnits(drawPosition + drawOffset); drawRotation = Timing.InterpolateRotation(prevRotation, body.Rotation) + rotationOffset; } public void CorrectPosition(List positionBuffer, out Vector2 newPosition, out Vector2 newVelocity, out float newRotation, out float newAngularVelocity) where T : PosInfo { newVelocity = LinearVelocity; newPosition = SimPosition; newRotation = Rotation; newAngularVelocity = AngularVelocity; while (positionBuffer.Count > 0 && positionBuffer[0].Timestamp < lastProcessedNetworkState) { positionBuffer.RemoveAt(0); } if (positionBuffer.Count == 0) { return; } lastProcessedNetworkState = positionBuffer[0].Timestamp; newVelocity = positionBuffer[0].LinearVelocity; newPosition = positionBuffer[0].Position; newRotation = positionBuffer[0].Rotation ?? Rotation; newAngularVelocity = positionBuffer[0].AngularVelocity ?? AngularVelocity; positionBuffer.RemoveAt(0); } /// /// Rotate the body towards the target rotation in the "shortest direction", taking into account the current angular velocity to prevent overshooting. /// /// Desired rotation in radians /// How fast the body should be rotated. Does not represent any real unit, you may want to experiment with different values to get the desired effect. /// Should the angles be wrapped. Set to false if it makes a difference whether the angle of the body is 0.0f or 360.0f. public void SmoothRotate(float targetRotation, float force = 10.0f, bool wrapAngle = true) { float nextAngle = body.Rotation + body.AngularVelocity * (float)Timing.Step; float angle = wrapAngle ? MathUtils.GetShortestAngle(nextAngle, targetRotation) : MathHelper.Clamp(targetRotation - nextAngle, -MathHelper.Pi, MathHelper.Pi); float torque = angle * 60.0f * (force / 100.0f); if (body.IsKinematic) { if (!IsValidValue(torque, "torque")) return; body.AngularVelocity = torque; } else { ApplyTorque(body.Mass * torque); } } public void Remove() { list.Remove(this); GameMain.World.RemoveBody(body); DisposeProjSpecific(); } public static void RemoveAll() { for (int i = list.Count - 1; i >= 0; i--) { list[i].Remove(); } System.Diagnostics.Debug.Assert(list.Count == 0); } public static bool IsValidShape(float radius, float height, float width) => radius > 0 || (height > 0 && width > 0); public static Shape DefineBodyShape(float radius, float width, float height) { Shape bodyShape; if (width <= 0 && height <= 0 && radius > 0) { bodyShape = Shape.Circle; } else if (radius > 0) { if (width > height) { bodyShape = Shape.HorizontalCapsule; } else { bodyShape = Shape.Capsule; } } else { bodyShape = Shape.Rectangle; } return bodyShape; } partial void DisposeProjSpecific(); } }