Files
RobustToolbox/Robust.Shared/Physics/Systems/SharedJointSystem.cs
T
83c2a1be11 [Dependency] source generator part 2 (#6550)
* [Dependency] source generator

No more reflection, no more codegen at runtime

Also various changes to Roslyn helpers to make this easier to write.

Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback.

No fallback is available for [field: Dependency] properties, due to a Roslyn bug.

Code Fixes exist. We love Roslyn

* Apply dependencies generator changes to all code

* Release notes

* Preprocessor got hands

* Handle nullable dependencies

These are bad but gotta deal with it.

* Apply suggestions from code review

Co-authored-by: Moony <moony@hellomouse.net>

* Fine, let's not use collection expressions

---------

Co-authored-by: Moony <moony@hellomouse.net>
2026-05-08 12:38:33 +02:00

649 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics.Contacts;
using Robust.Shared.Physics.Dynamics.Joints;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Systems;
public abstract partial class SharedJointSystem : EntitySystem
{
[Dependency] private SharedContainerSystem _container = default!;
[Dependency] private SharedPhysicsSystem _physics = default!;
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private SharedTransformSystem _transform = default!;
private EntityQuery<JointComponent> _jointsQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<JointRelayTargetComponent> _relayQuery;
// To avoid issues with component states we'll queue up all dirty joints and check it every tick to see if
// we can delete the component.
private readonly HashSet<Entity<JointComponent>> _dirtyJoints = new();
protected readonly HashSet<Joint> AddedJoints = new();
protected readonly List<Joint> ToRemove = new();
public override void Initialize()
{
base.Initialize();
_jointsQuery = GetEntityQuery<JointComponent>();
_relayQuery = GetEntityQuery<JointRelayTargetComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
UpdatesOutsidePrediction = true;
UpdatesBefore.Add(typeof(SharedPhysicsSystem));
SubscribeLocalEvent<JointComponent, ComponentShutdown>(OnJointShutdown);
SubscribeLocalEvent<JointComponent, ComponentInit>(OnJointInit);
InitializeRelay();
}
#region Lifetime
private void OnJointInit(EntityUid uid, JointComponent component, ComponentInit args)
{
foreach (var (id, joint) in component.Joints)
{
var other = uid == joint.BodyAUid ? joint.BodyBUid : joint.BodyAUid;
// Client may not yet know about the other entity.
// But whenever that other entity enters PVS, its own joint initialization should hopefully run this again anyways.
if (!TryComp(joint.BodyAUid, out PhysicsComponent? bodyA) || !TryComp(joint.BodyBUid, out PhysicsComponent? bodyB) || !TryComp(other, out JointComponent? otherComp))
continue;
if (!otherComp.Joints.ContainsKey(id))
{
// This can happen if the other joint handled its state before this entity was initialized. In this
// case we need to re-add the joint to the other entity.
if (uid == joint.BodyAUid)
InitJoint(joint, bodyA, bodyB, component, otherComp, ignoreExisting: true);
else
InitJoint(joint, bodyA, bodyB, otherComp, component, ignoreExisting: true);
continue;
}
_physics.WakeBody(joint.BodyAUid, body: bodyA);
_physics.WakeBody(joint.BodyBUid, body: bodyB);
// Raise broadcast last so we can do both sides of directed first.
var vera = new JointAddedEvent(joint, joint.BodyAUid, joint.BodyBUid, bodyA, bodyB);
RaiseLocalEvent(joint.BodyAUid, vera);
var smug = new JointAddedEvent(joint, joint.BodyBUid, joint.BodyAUid, bodyB, bodyA);
RaiseLocalEvent(joint.BodyBUid, smug);
EntityManager.EventBus.RaiseEvent(EventSource.Local, vera);
}
RefreshRelay(uid, component);
}
private void OnJointShutdown(EntityUid uid, JointComponent component, ComponentShutdown args)
{
foreach (var joint in component.Joints.Values)
{
RemoveJoint(joint);
}
// If we're relaying elsewhere then cleanup our old data.
if (component.Relay != null && !TerminatingOrDeleted(component.Relay.Value))
SetRelay(uid, null, component);
}
#endregion
public void SetEnabled(Joint joint, bool value)
{
if (joint.Enabled == value)
return;
joint.Enabled = value;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var joint in AddedJoints)
{
InitJoint(joint);
}
AddedJoints.Clear();
foreach (var joint in _dirtyJoints)
{
if (joint.Comp.Deleted || joint.Comp.JointCount != 0) continue;
RemComp<JointComponent>(joint);
}
_dirtyJoints.Clear();
}
private void InitJoint(Joint joint,
PhysicsComponent? bodyA = null,
PhysicsComponent? bodyB = null,
JointComponent? jointComponentA = null,
JointComponent? jointComponentB = null,
bool ignoreExisting = false)
{
var aUid = joint.BodyAUid;
var bUid = joint.BodyBUid;
if (!_physicsQuery.Resolve(aUid, ref bodyA, false) || !_physicsQuery.Resolve(bUid, ref bodyB, false))
return;
DebugTools.Assert(Transform(aUid).MapID == Transform(bUid).MapID, "Attempted to initialize cross-map joint");
jointComponentA ??= EnsureComp<JointComponent>(aUid);
jointComponentB ??= EnsureComp<JointComponent>(bUid);
DebugTools.AssertOwner(aUid, jointComponentA);
DebugTools.AssertOwner(bUid, jointComponentB);
DebugTools.AssertNotEqual(jointComponentA.Relay, bUid);
DebugTools.AssertNotEqual(jointComponentB.Relay, aUid);
var jointsA = jointComponentA.Joints;
var jointsB = jointComponentB.Joints;
if (_gameTiming.IsFirstTimePredicted)
Log.Debug($"Initializing joint {joint.ID}");
// Check for existing joints
if (!ignoreExisting && jointsA.TryGetValue(joint.ID, out var existing))
{
if (existing.BodyBUid != bUid)
{
Log.Error($"While adding joint {joint.ID} to entity {ToPrettyString(bUid)}, the connected entity {ToPrettyString(aUid)} already had a joint with the same ID connected to another entity {ToPrettyString(existing.BodyBUid)}.");
return;
}
// If they both already have it we should be gucci
// This can occur because of client states coming in blah blah
// The reason for this is we defer everything until Update
// (and the reason we defer is to avoid modifying components during iteration when we do the EnsureComponent)
if (jointsB.TryGetValue(joint.ID, out var value))
{
DebugTools.Assert(value.BodyAUid == aUid);
return;
}
Log.Error($"While adding joint {joint.ID} to entity {ToPrettyString(bUid)}, the joint already existed for the connected entity {ToPrettyString(aUid)}.");
}
else if (!ignoreExisting && jointsB.TryGetValue(joint.ID, out existing))
{
if (existing.BodyAUid != aUid)
{
Log.Error($"While adding joint {joint.ID} to entity {ToPrettyString(aUid)}, the connected entity {ToPrettyString(bUid)} already had a joint with the same ID connected to another entity {ToPrettyString(existing.BodyAUid)}.");
return;
}
Log.Error($"While adding joint {joint.ID} to entity {ToPrettyString(aUid)}, the joint already existed for the connected entity {ToPrettyString(bUid)}.");
}
jointsA.TryAdd(joint.ID, joint);
jointsB.TryAdd(joint.ID, joint);
// If the joint prevents collisions, then flag any contacts for filtering.
if (!joint.CollideConnected)
{
FilterContactsForJoint(joint, bodyA, bodyB);
}
// TODO reduce metadata resolves.
_physics.WakeBody(aUid, body: bodyA);
_physics.WakeBody(bUid, body: bodyB);
Dirty(aUid, bodyA);
Dirty(bUid, bodyB);
Dirty(aUid, jointComponentA);
Dirty(bUid, jointComponentB);
// Also flag these for checking juusssttt in case.
_dirtyJoints.Add((aUid, jointComponentA));
_dirtyJoints.Add((bUid, jointComponentB));
// Note: creating a joint doesn't wake the bodies.
// Raise broadcast last so we can do both sides of directed first.
var vera = new JointAddedEvent(joint, aUid, bUid, bodyA, bodyB);
EntityManager.EventBus.RaiseLocalEvent(aUid, vera);
var smug = new JointAddedEvent(joint, bUid, aUid, bodyB, bodyA);
EntityManager.EventBus.RaiseLocalEvent(bUid, smug);
EntityManager.EventBus.RaiseEvent(EventSource.Local, vera);
}
private static string GetJointId(Joint joint)
{
var id = joint.ID;
return !string.IsNullOrEmpty(id) ? id : joint.GetHashCode().ToString();
}
#region Helpers
/// <summary>
/// Create a DistanceJoint between 2 bodies. This should be called content-side whenever you need one.
/// </summary>
public DistanceJoint CreateDistanceJoint(
EntityUid bodyA,
EntityUid bodyB,
Vector2? anchorA = null,
Vector2? anchorB = null,
string? id = null,
TransformComponent? xformA = null,
TransformComponent? xformB = null,
float? minimumDistance = null)
{
if (!Resolve(bodyA, ref xformA) || !Resolve(bodyB, ref xformB))
{
throw new InvalidOperationException();
}
anchorA ??= Vector2.Zero;
anchorB ??= Vector2.Zero;
var vecA = Vector2.Transform(anchorA.Value, _transform.GetWorldMatrix(xformA));
var vecB = Vector2.Transform(anchorB.Value, _transform.GetWorldMatrix(xformB));
var length = (vecA - vecB).Length();
if (minimumDistance != null)
length = Math.Max(minimumDistance.Value, length);
var joint = new DistanceJoint(bodyA, bodyB, anchorA.Value, anchorB.Value, length);
id ??= GetJointId(joint);
joint.ID = id;
AddJoint(joint);
return joint;
}
/// <summary>
/// Create a MouseJoint between 2 bodies. This should be called content-side whenever you need one.
/// </summary>
public MouseJoint CreateMouseJoint(EntityUid bodyA, EntityUid bodyB, Vector2? anchorA = null, Vector2? anchorB = null, string? id = null)
{
anchorA ??= Vector2.Zero;
anchorB ??= Vector2.Zero;
var joint = new MouseJoint(bodyA, bodyB, anchorA.Value, anchorB.Value);
id ??= GetJointId(joint);
joint.ID = id;
AddJoint(joint);
return joint;
}
public PrismaticJoint CreatePrismaticJoint(EntityUid bodyA, EntityUid bodyB, string? id = null)
{
var joint = new PrismaticJoint(bodyA, bodyB);
id ??= GetJointId(joint);
joint.ID = id;
AddJoint(joint);
return joint;
}
public PrismaticJoint CreatePrismaticJoint(
EntityUid bodyA,
EntityUid bodyB,
Vector2 anchorA,
Vector2 anchorB,
Vector2 worldAxis,
float referenceAngle,
string? id = null)
{
var axis = GetLocalVector2(bodyA, worldAxis);
var joint = new PrismaticJoint(bodyA, bodyB, anchorA, anchorB, axis, referenceAngle);
id ??= GetJointId(joint);
joint.ID = id;
AddJoint(joint);
return joint;
}
public RevoluteJoint CreateRevoluteJoint(EntityUid bodyA, EntityUid bodyB, string? id = null)
{
var joint = new RevoluteJoint(bodyA, bodyB);
id ??= GetJointId(joint);
joint.ID = id;
AddJoint(joint);
return joint;
}
public WeldJoint GetOrCreateWeldJoint(EntityUid bodyA, EntityUid bodyB, string? id = null)
{
if (id != null &&
_jointsQuery.TryComp(bodyA, out JointComponent? jointComponent) &&
jointComponent.Joints.TryGetValue(id, out var weldJoint))
{
return (WeldJoint) weldJoint;
}
var joint = new WeldJoint(bodyA, bodyB);
id ??= GetJointId(joint);
joint.ID = id;
AddJoint(joint);
return joint;
}
public WeldJoint CreateWeldJoint(EntityUid bodyA, EntityUid bodyB, string? id = null)
{
var joint = new WeldJoint(bodyA, bodyB);
id ??= GetJointId(joint);
joint.ID = id;
AddJoint(joint);
return joint;
}
private Vector2 GetLocalVector2(EntityUid uid, Vector2 worldVector, TransformComponent? xform = null)
{
if (!Resolve(uid, ref xform))
return Vector2.Zero;
return Physics.Transform.MulT(new Quaternion2D((float) _transform.GetWorldRotation(xform).Theta), worldVector);
}
#endregion
public static void LinearStiffness(
float frequencyHertz,
float dampingRatio,
float massA,
float massB,
out float stiffness, out float damping)
{
float mass;
if (massA > 0.0f && massB > 0.0f)
{
mass = massA * massB / (massA + massB);
}
else if (massA > 0.0f)
{
mass = massA;
}
else
{
mass = massB;
}
var omega = 2.0f * MathF.PI * frequencyHertz;
stiffness = mass * omega * omega;
damping = 2.0f * mass * dampingRatio * omega;
}
public static void AngularStiffness(
float frequencyHertz,
float dampingRatio,
PhysicsComponent bodyA,
PhysicsComponent bodyB,
out float stiffness, out float damping)
{
var IA = bodyA.Inertia;
var IB = bodyB.Inertia;
float I;
if (IA > 0.0f && IB > 0.0f)
{
I = IA * IB / (IA + IB);
}
else if (IA > 0.0f)
{
I = IA;
}
else
{
I = IB;
}
float omega = 2.0f * MathF.PI * frequencyHertz;
stiffness = I * omega * omega;
damping = 2.0f * I * dampingRatio * omega;
}
#region Joints
protected void AddJoint(Joint joint, PhysicsComponent? bodyA = null, PhysicsComponent? bodyB = null)
{
if (!_physicsQuery.Resolve(joint.BodyAUid, ref bodyA) || !_physicsQuery.Resolve(joint.BodyBUid, ref bodyB))
return;
if (!joint.CollideConnected)
FilterContactsForJoint(joint, bodyA, bodyB);
// Maybe make this method AddOrUpdate so we can have an Add one that explicitly throws if present?
var mapidA = Transform(joint.BodyAUid).MapID;
if (mapidA == MapId.Nullspace ||
mapidA != Transform(joint.BodyBUid).MapID)
{
Log.Error($"Tried to add joint to ineligible bodies");
return;
}
if (string.IsNullOrEmpty(joint.ID))
{
Log.Error($"Can't add a joint with no ID");
DebugTools.Assert($"Can't add a joint with no ID");
return;
}
InitJoint(joint, bodyA, bodyB);
if (_gameTiming.IsFirstTimePredicted)
{
Log.Debug($"Added {joint.JointType} Joint with ID {joint.ID} from {bodyA.BodyType} to {bodyB.BodyType} ");
}
}
/// <summary>
/// Removes joints on this entity and anything relaying to it.
/// </summary>
public void RecursiveClearJoints(
EntityUid uid,
TransformComponent? xform = null,
JointComponent? component = null,
JointRelayTargetComponent? relay = null)
{
if (!Resolve(uid, ref xform))
return;
_jointsQuery.Resolve(uid, ref component, false);
_relayQuery.Resolve(uid, ref relay, false);
if (relay != null)
{
foreach (var ree in relay.Relayed)
{
_jointsQuery.TryGetComponent(ree, out var rJoint);
ClearJoints(ree, rJoint);
}
RemComp(uid, relay);
}
if (component != null)
{
ClearJoints(uid, component);
}
}
/// <summary>
/// Clears any joints for this entity, excluding relayed joints.
/// </summary>
public void ClearJoints(EntityUid uid, JointComponent? component = null)
{
if (!_jointsQuery.Resolve(uid, ref component, false))
return;
// TODO PERFORMANCE
// This will re-fetch the joint & body component for this entity ( & ever connected
// entity), for each and every joint. at the very least, we could pass in the joint & physics comp. As long
// as most entities only have a single joint, fetching connected components probably isn't worth it.
foreach (var a in component.Joints.Values.ToArray())
{
RemoveJoint(a);
}
foreach (var j in AddedJoints)
{
if (j.BodyAUid == uid || j.BodyBUid == uid)
ToRemove.Add(j);
}
AddedJoints.ExceptWith(ToRemove);
ToRemove.Clear();
if (_gameTiming.IsFirstTimePredicted)
{
Log.Debug($"Removed all joints from entity {ToPrettyString(uid)}");
}
}
/// <summary>
/// Removes any joints attached to this object with a specified ID.
/// </summary>
/// <param name="uid">Entity UID of the object to remove the joint from</param>
/// <param name="id">ID of the joint to remove</param>
/// <param name="recursive">Whether to also remove any relayed joints. Optional, defaults to true</param>
public void RemoveJoint(EntityUid uid, string id, bool recursive = true)
{
if (_jointsQuery.TryComp(uid, out var jointComp) &&
jointComp.Joints.TryGetValue(id, out var joint))
RemoveJoint(joint);
if (!recursive || !_relayQuery.TryComp(uid, out var relayComp))
return;
foreach (var relay in relayComp.Relayed)
{
if(_jointsQuery.TryComp(relay, out var relayJointComp))
{
if(relayJointComp.Joints.TryGetValue(id, out var relayJoint))
RemoveJoint(relayJoint);
}
}
}
/// <summary>
/// Removes a specific joint from existence
/// </summary>
/// <param name="joint">The joint to remove</param>
public void RemoveJoint(Joint joint)
{
AddedJoints.Remove(joint);
var bodyAUid = joint.BodyAUid;
var bodyBUid = joint.BodyBUid;
// Originally I logged these but because of prediction the client can just nuke them multiple times in a row
// because each body has its own JointComponent, bleh.
if (!_jointsQuery.TryComp(bodyAUid, out var jointComponentA))
{
return;
}
if (!_jointsQuery.TryComp(bodyBUid, out var jointComponentB))
{
return;
}
if (!jointComponentA.Joints.Remove(joint.ID))
{
return;
}
if (!jointComponentB.Joints.Remove(joint.ID))
{
return;
}
// Wake up connected bodies.
if (_physicsQuery.TryComp(bodyAUid, out var bodyA) &&
MetaData(bodyAUid).EntityLifeStage < EntityLifeStage.Terminating)
{
var uidA = jointComponentA.Relay ?? bodyAUid;
_physics.WakeBody(uidA);
}
if (TryComp<PhysicsComponent>(bodyBUid, out var bodyB) &&
MetaData(bodyBUid).EntityLifeStage < EntityLifeStage.Terminating)
{
var uidB = jointComponentB.Relay ?? bodyBUid;
_physics.WakeBody(uidB);
}
if (!jointComponentA.Deleted)
{
Dirty(bodyAUid, jointComponentA);
}
if (!jointComponentB.Deleted)
{
Dirty(bodyBUid, jointComponentB);
}
if (jointComponentA.Deleted && jointComponentB.Deleted)
return;
// If the joint prevents collisions, then flag any contacts for filtering.
if (!joint.CollideConnected)
{
FilterContactsForJoint(joint);
}
if (bodyA == null)
{
Log.Debug($"Tried to remove joint from entity {ToPrettyString(bodyAUid)} without a physics component");
}
else if (bodyB == null)
{
Log.Debug($"Tried to remove joint from entity {ToPrettyString(bodyBUid)} without a physics component");
}
else
{
var vera = new JointRemovedEvent(joint, bodyAUid, bodyBUid, bodyA, bodyB);
EntityManager.EventBus.RaiseLocalEvent(bodyAUid, vera, false);
var smug = new JointRemovedEvent(joint, bodyBUid, bodyAUid, bodyB, bodyA);
EntityManager.EventBus.RaiseLocalEvent(bodyBUid, smug, false);
EntityManager.EventBus.RaiseEvent(EventSource.Local, vera);
if (_gameTiming.IsFirstTimePredicted)
{
Log.Debug($"Removed {joint.JointType} joint with ID {joint.ID} from entity {ToPrettyString(bodyAUid)} to entity {ToPrettyString(bodyBUid)}");
}
}
// We can't just check up front due to how prediction works.
_dirtyJoints.Add((bodyAUid, jointComponentA));
_dirtyJoints.Add((bodyBUid, jointComponentB));
}
#endregion
internal void FilterContactsForJoint(Joint joint, PhysicsComponent? bodyA = null, PhysicsComponent? bodyB = null)
{
if (!_physicsQuery.Resolve(joint.BodyBUid, ref bodyB))
return;
var node = bodyB.Contacts.First;
while (node != null)
{
var contact = node.Value;
node = node.Next;
if (contact.EntityA == joint.BodyAUid ||
contact.EntityB == joint.BodyAUid)
{
// Flag the contact for filtering at the next time step (where either
// body is awake).
contact.Flags |= ContactFlags.Filter;
}
}
}
}