using System; using Prometheus; using Robust.Shared.Configuration; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Timing; using Robust.Shared.Utility; using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute; namespace Robust.Shared.GameObjects { public abstract partial class SharedPhysicsSystem : EntitySystem { /* * TODO: * Raycasts for non-box shapes. * SetTransformIgnoreContacts for teleports (and anything else left on the physics body in Farseer) * TOI Solver (continuous collision detection) * Poly cutting * Chain shape */ public static readonly Histogram TickUsageControllerBeforeSolveHistogram = Metrics.CreateHistogram("robust_entity_physics_controller_before_solve", "Amount of time spent running a controller's UpdateBeforeSolve", new HistogramConfiguration { LabelNames = new[] {"controller"}, Buckets = Histogram.ExponentialBuckets(0.000_001, 1.5, 25) }); public static readonly Histogram TickUsageControllerAfterSolveHistogram = Metrics.CreateHistogram("robust_entity_physics_controller_after_solve", "Amount of time spent running a controller's UpdateAfterSolve", new HistogramConfiguration { LabelNames = new[] {"controller"}, Buckets = Histogram.ExponentialBuckets(0.000_001, 1.5, 25) }); [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!; [Dependency] private readonly SharedJointSystem _joints = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] protected readonly IMapManager MapManager = default!; [Dependency] private readonly IPhysicsManager _physicsManager = default!; public Action? KinematicControllerCollision; public bool MetricsEnabled { get; protected set; } private readonly Stopwatch _stopwatch = new(); private ISawmill _sawmill = default!; public override void Initialize() { base.Initialize(); _sawmill = Logger.GetSawmill("physics"); _sawmill.Level = LogLevel.Info; SubscribeLocalEvent(ev => { if (ev.Created) HandleMapCreated(ev); }); SubscribeLocalEvent(HandleGridInit); SubscribeLocalEvent(HandlePhysicsUpdateMessage); SubscribeLocalEvent(OnWake); SubscribeLocalEvent(OnSleep); SubscribeLocalEvent(HandleContainerInserted); SubscribeLocalEvent(HandleContainerRemoved); SubscribeLocalEvent(OnParentChange); SubscribeLocalEvent(HandlePhysicsMapInit); SubscribeLocalEvent(HandlePhysicsMapRemove); SubscribeLocalEvent(OnPhysicsInit); SubscribeLocalEvent(OnPhysicsGetState); SubscribeLocalEvent(OnPhysicsHandleState); IoCManager.Resolve().Initialize(); var configManager = IoCManager.Resolve(); configManager.OnValueChanged(CVars.AutoClearForces, OnAutoClearChange); } private void HandlePhysicsMapInit(EntityUid uid, SharedPhysicsMapComponent component, ComponentInit args) { IoCManager.InjectDependencies(component); component.BroadphaseSystem = _broadphase; component._physics = this; component.ContactManager = new(); component.ContactManager.Initialize(); component.ContactManager.MapId = component.MapId; component.AutoClearForces = IoCManager.Resolve().GetCVar(CVars.AutoClearForces); component.ContactManager.KinematicControllerCollision += KinematicControllerCollision; } private void OnAutoClearChange(bool value) { foreach (var component in EntityManager.EntityQuery(true)) { component.AutoClearForces = value; } } private void HandlePhysicsMapRemove(EntityUid uid, SharedPhysicsMapComponent component, ComponentRemove args) { component.ContactManager.KinematicControllerCollision -= KinematicControllerCollision; component.ContactManager.Shutdown(); } private void OnParentChange(EntityUid uid, PhysicsComponent body, ref EntParentChangedMessage args) { var meta = MetaData(uid); if (meta.EntityLifeStage < EntityLifeStage.Initialized || !TryComp(uid, out TransformComponent? xform)) { return; } if (body._canCollide) _broadphase.UpdateBroadphase(body, xform: xform); // Handle map change var mapId = _transform.GetMapId(args.Entity); if (args.OldMapId != mapId) HandleMapChange(body, xform, args.OldMapId, mapId); if (body.BodyType != BodyType.Static && mapId != MapId.Nullspace && body._canCollide) HandleParentChangeVelocity(uid, body, ref args, xform); } private void HandleMapChange(PhysicsComponent body, TransformComponent xform, MapId oldMapId, MapId mapId) { _joints.ClearJoints(body); // So if the map is being deleted it detaches all of its bodies to null soooo we have this fun check. SharedPhysicsMapComponent? oldMap = null; SharedPhysicsMapComponent? map = null; if (oldMapId != MapId.Nullspace) { var oldMapEnt = MapManager.GetMapEntityId(oldMapId); if (TryComp(oldMapEnt, out var meta) && meta.EntityLifeStage < EntityLifeStage.Terminating) { oldMap = Comp(oldMapEnt); oldMap.RemoveBody(body); } } if (mapId != MapId.Nullspace) { map = Comp(MapManager.GetMapEntityId(mapId)); map.AddBody(body); } if (xform.ChildCount == 0 || (oldMap == null && map == null) || MapManager.IsGrid(body.Owner) || MapManager.IsMap(body.Owner)) return; var xformQuery = GetEntityQuery(); var bodyQuery = GetEntityQuery(); var metaQuery = GetEntityQuery(); RecursiveMapUpdate(xform, oldMap, map, xformQuery, bodyQuery, metaQuery); } private void RecursiveMapUpdate( TransformComponent xform, SharedPhysicsMapComponent? oldMap, SharedPhysicsMapComponent? map, EntityQuery xformQuery, EntityQuery bodyQuery, EntityQuery metaQuery) { var childEnumerator = xform.ChildEnumerator; while (childEnumerator.MoveNext(out var child)) { if (!bodyQuery.TryGetComponent(child.Value, out var childBody) || !xformQuery.TryGetComponent(child.Value, out var childXform) || metaQuery.GetComponent(child.Value).EntityLifeStage == EntityLifeStage.Deleted) continue; _joints.ClearJoints(childBody); oldMap?.RemoveBody(childBody); map?.AddBody(childBody); RecursiveMapUpdate(childXform, oldMap, map, xformQuery, bodyQuery, metaQuery); } } private void HandleGridInit(GridInitializeEvent ev) { if (!EntityManager.EntityExists(ev.EntityUid)) return; // Yes this ordering matters var collideComp = EntityManager.EnsureComponent(ev.EntityUid); collideComp.BodyType = BodyType.Static; EntityManager.EnsureComponent(ev.EntityUid); } public override void Shutdown() { base.Shutdown(); var configManager = IoCManager.Resolve(); configManager.UnsubValueChanged(CVars.AutoClearForces, OnAutoClearChange); } protected abstract void HandleMapCreated(MapChangedEvent eventArgs); private void HandlePhysicsUpdateMessage(CollisionChangeMessage message) { var mapId = Transform(message.Owner).MapID; if (mapId == MapId.Nullspace) return; var physicsMap = Comp(MapManager.GetMapEntityId(mapId)); if (Deleted(message.Owner) || !message.CanCollide) { physicsMap.RemoveBody(message.Body); } else { physicsMap.AddBody(message.Body); } } private void OnWake(ref PhysicsWakeEvent @event) { var mapId = EntityManager.GetComponent(@event.Body.Owner).MapID; if (mapId == MapId.Nullspace) return; EntityUid tempQualifier = MapManager.GetMapEntityId(mapId); EntityManager.GetComponent(tempQualifier).AddAwakeBody(@event.Body); } private void OnSleep(ref PhysicsSleepEvent @event) { var mapId = EntityManager.GetComponent(@event.Body.Owner).MapID; if (mapId == MapId.Nullspace) return; EntityUid tempQualifier = MapManager.GetMapEntityId(mapId); EntityManager.GetComponent(tempQualifier).RemoveSleepBody(@event.Body); } private void HandleContainerInserted(EntInsertedIntoContainerMessage message) { if (!EntityManager.TryGetComponent(message.Entity, out PhysicsComponent? physicsComponent)) return; var mapId = EntityManager.GetComponent(message.Container.Owner).MapID; physicsComponent.LinearVelocity = Vector2.Zero; physicsComponent.AngularVelocity = 0.0f; _joints.ClearJoints(physicsComponent); if (mapId != MapId.Nullspace) { EntityUid tempQualifier = MapManager.GetMapEntityId(mapId); EntityManager.GetComponent(tempQualifier).RemoveBody(physicsComponent); } } private void HandleContainerRemoved(EntRemovedFromContainerMessage message) { // If entity being deleted then the parent change will already be handled elsewhere and we don't want to re-add it to the map. if (!EntityManager.TryGetComponent(message.Entity, out PhysicsComponent? physicsComponent) || MetaData(message.Entity).EntityLifeStage >= EntityLifeStage.Terminating) return; var mapId = Transform(message.Container.Owner).MapID; if (mapId != MapId.Nullspace) { DebugTools.Assert(!physicsComponent.Deleted); var tempQualifier = MapManager.GetMapEntityId(mapId); Comp(tempQualifier).AddBody(physicsComponent); } } /// /// Simulates the physical world for a given amount of time. /// /// Delta Time in seconds of how long to simulate the world. /// Should only predicted entities be considered in this simulation step? protected void SimulateWorld(float deltaTime, bool prediction) { var updateBeforeSolve = new PhysicsUpdateBeforeSolveEvent(prediction, deltaTime); RaiseLocalEvent(ref updateBeforeSolve); foreach (var comp in EntityManager.EntityQuery(true)) { comp.Step(deltaTime, prediction); } var updateAfterSolve = new PhysicsUpdateAfterSolveEvent(prediction, deltaTime); RaiseLocalEvent(ref updateAfterSolve); // Go through and run all of the deferred events now foreach (var comp in EntityManager.EntityQuery(true)) { comp.ProcessQueue(); } _physicsManager.ClearTransforms(); } internal static (int Batches, int BatchSize) GetBatch(int count, int minimumBatchSize) { var batches = Math.Min( (int) MathF.Ceiling((float) count / minimumBatchSize), Math.Max(1, Environment.ProcessorCount)); var batchSize = (int) MathF.Ceiling((float) count / batches); return (batches, batchSize); } } [ByRefEvent] public readonly struct PhysicsUpdateAfterSolveEvent { public readonly bool Prediction; public readonly float DeltaTime; public PhysicsUpdateAfterSolveEvent(bool prediction, float deltaTime) { Prediction = prediction; DeltaTime = deltaTime; } } [ByRefEvent] public readonly struct PhysicsUpdateBeforeSolveEvent { public readonly bool Prediction; public readonly float DeltaTime; public PhysicsUpdateBeforeSolveEvent(bool prediction, float deltaTime) { Prediction = prediction; DeltaTime = deltaTime; } } }