PullController.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. using System.Numerics;
  2. using Content.Server.Movement.Components;
  3. using Content.Server.Physics.Controllers;
  4. using Content.Shared.ActionBlocker;
  5. using Content.Shared.Gravity;
  6. using Content.Shared.Input;
  7. using Content.Shared.Movement.Pulling.Components;
  8. using Content.Shared.Movement.Pulling.Events;
  9. using Content.Shared.Movement.Pulling.Systems;
  10. using Content.Shared.Rotatable;
  11. using Robust.Server.Physics;
  12. using Robust.Shared.Containers;
  13. using Robust.Shared.Input.Binding;
  14. using Robust.Shared.Map;
  15. using Robust.Shared.Physics;
  16. using Robust.Shared.Physics.Components;
  17. using Robust.Shared.Physics.Controllers;
  18. using Robust.Shared.Physics.Dynamics.Joints;
  19. using Robust.Shared.Player;
  20. using Robust.Shared.Timing;
  21. using Robust.Shared.Utility;
  22. namespace Content.Server.Movement.Systems;
  23. public sealed class PullController : VirtualController
  24. {
  25. /*
  26. * This code is awful. If you try to tweak this without refactoring it I'm gonna revert it.
  27. */
  28. // Parameterization for pulling:
  29. // Speeds. Note that the speed is mass-independent (multiplied by mass).
  30. // Instead, tuning to mass is done via the mass values below.
  31. // Note that setting the speed too high results in overshoots (stabilized by drag, but bad)
  32. private const float AccelModifierHigh = 15f;
  33. private const float AccelModifierLow = 60.0f;
  34. // High/low-mass marks. Curve is constant-lerp-constant, i.e. if you can even pull an item,
  35. // you'll always get at least AccelModifierLow and no more than AccelModifierHigh.
  36. private const float AccelModifierHighMass = 70.0f; // roundstart saltern emergency closet
  37. private const float AccelModifierLowMass = 5.0f; // roundstart saltern emergency crowbar
  38. // Used to control settling (turns off pulling).
  39. private const float MaximumSettleVelocity = 0.1f;
  40. private const float MaximumSettleDistance = 0.1f;
  41. // Settle shutdown control.
  42. // Mustn't be too massive, as that causes severe mispredicts *and can prevent it ever resolving*.
  43. // Exists to bleed off "I pulled my crowbar" overshoots.
  44. // Minimum velocity for shutdown to be necessary. This prevents stuff getting stuck b/c too much shutdown.
  45. private const float SettleMinimumShutdownVelocity = 0.25f;
  46. // Distance in which settle shutdown multiplier is at 0. It then scales upwards linearly with closer distances.
  47. private const float SettleShutdownDistance = 1.0f;
  48. // Velocity change of -LinearVelocity * frameTime * this
  49. private const float SettleShutdownMultiplier = 20.0f;
  50. // How much you must move for the puller movement check to actually hit.
  51. private const float MinimumMovementDistance = 0.005f;
  52. [Dependency] private readonly IGameTiming _timing = default!;
  53. [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
  54. [Dependency] private readonly SharedContainerSystem _container = default!;
  55. [Dependency] private readonly SharedGravitySystem _gravity = default!;
  56. [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
  57. /// <summary>
  58. /// If distance between puller and pulled entity lower that this threshold,
  59. /// pulled entity will not change its rotation.
  60. /// Helps with small distance jittering
  61. /// </summary>
  62. private const float ThresholdRotDistance = 1;
  63. /// <summary>
  64. /// If difference between puller and pulled angle lower that this threshold,
  65. /// pulled entity will not change its rotation.
  66. /// Helps with diagonal movement jittering
  67. /// As of further adjustments, should divide cleanly into 90 degrees
  68. /// </summary>
  69. private const float ThresholdRotAngle = 22.5f;
  70. private EntityQuery<PhysicsComponent> _physicsQuery;
  71. private EntityQuery<PullableComponent> _pullableQuery;
  72. private EntityQuery<PullerComponent> _pullerQuery;
  73. private EntityQuery<TransformComponent> _xformQuery;
  74. public override void Initialize()
  75. {
  76. CommandBinds.Builder
  77. .Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(OnRequestMovePulledObject))
  78. .Register<PullingSystem>();
  79. _physicsQuery = GetEntityQuery<PhysicsComponent>();
  80. _pullableQuery = GetEntityQuery<PullableComponent>();
  81. _pullerQuery = GetEntityQuery<PullerComponent>();
  82. _xformQuery = GetEntityQuery<TransformComponent>();
  83. UpdatesAfter.Add(typeof(MoverController));
  84. SubscribeLocalEvent<PullMovingComponent, PullStoppedMessage>(OnPullStop);
  85. SubscribeLocalEvent<ActivePullerComponent, MoveEvent>(OnPullerMove);
  86. base.Initialize();
  87. }
  88. public override void Shutdown()
  89. {
  90. base.Shutdown();
  91. CommandBinds.Unregister<PullController>();
  92. }
  93. private void OnPullStop(Entity<PullMovingComponent> ent, ref PullStoppedMessage args)
  94. {
  95. RemCompDeferred<PullMovingComponent>(ent);
  96. }
  97. private bool OnRequestMovePulledObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
  98. {
  99. if (session?.AttachedEntity is not { } player ||
  100. !player.IsValid())
  101. {
  102. return false;
  103. }
  104. if (!_pullerQuery.TryComp(player, out var pullerComp))
  105. return false;
  106. var pulled = pullerComp.Pulling;
  107. if (!_pullableQuery.TryComp(pulled, out var pullable))
  108. return false;
  109. if (_container.IsEntityInContainer(player))
  110. return false;
  111. pullerComp.NextThrow = _timing.CurTime + pullerComp.ThrowCooldown;
  112. // Cap the distance
  113. var range = 2f;
  114. var fromUserCoords = coords.WithEntityId(player, EntityManager);
  115. var userCoords = new EntityCoordinates(player, Vector2.Zero);
  116. if (!_transformSystem.InRange(coords, userCoords, range))
  117. {
  118. var direction = fromUserCoords.Position - userCoords.Position;
  119. // TODO: Joint API not ass
  120. // with that being said I think throwing is the way to go but.
  121. if (pullable.PullJointId != null &&
  122. TryComp(player, out JointComponent? joint) &&
  123. joint.GetJoints.TryGetValue(pullable.PullJointId, out var pullJoint) &&
  124. pullJoint is DistanceJoint distance)
  125. {
  126. range = MathF.Max(0.01f, distance.MaxLength - 0.01f);
  127. }
  128. fromUserCoords = new EntityCoordinates(player, direction.Normalized() * (range - 0.01f));
  129. coords = fromUserCoords.WithEntityId(coords.EntityId);
  130. }
  131. var moving = EnsureComp<PullMovingComponent>(pulled!.Value);
  132. moving.MovingTo = coords;
  133. return false;
  134. }
  135. private void OnPullerMove(EntityUid uid, ActivePullerComponent component, ref MoveEvent args)
  136. {
  137. if (!_pullerQuery.TryComp(uid, out var puller))
  138. return;
  139. if (puller.Pulling is not { } pullable)
  140. {
  141. DebugTools.Assert($"Failed to clean up puller: {ToPrettyString(uid)}");
  142. RemCompDeferred(uid, component);
  143. return;
  144. }
  145. UpdatePulledRotation(uid, pullable);
  146. // WHY
  147. if (args.NewPosition.EntityId == args.OldPosition.EntityId &&
  148. (args.NewPosition.Position - args.OldPosition.Position).LengthSquared() <
  149. MinimumMovementDistance * MinimumMovementDistance)
  150. {
  151. return;
  152. }
  153. if (_physicsQuery.TryComp(uid, out var physics))
  154. PhysicsSystem.WakeBody(uid, body: physics);
  155. RemCompDeferred<PullMovingComponent>(pullable);
  156. }
  157. private void UpdatePulledRotation(EntityUid puller, EntityUid pulled)
  158. {
  159. // TODO: update once ComponentReference works with directed event bus.
  160. if (!TryComp(pulled, out RotatableComponent? rotatable))
  161. return;
  162. if (!rotatable.RotateWhilePulling)
  163. return;
  164. var pulledXform = _xformQuery.GetComponent(pulled);
  165. var pullerXform = _xformQuery.GetComponent(puller);
  166. var pullerData = TransformSystem.GetWorldPositionRotation(pullerXform);
  167. var pulledData = TransformSystem.GetWorldPositionRotation(pulledXform);
  168. var dir = pullerData.WorldPosition - pulledData.WorldPosition;
  169. if (dir.LengthSquared() > ThresholdRotDistance * ThresholdRotDistance)
  170. {
  171. var oldAngle = pulledData.WorldRotation;
  172. var newAngle = Angle.FromWorldVec(dir);
  173. var diff = newAngle - oldAngle;
  174. if (Math.Abs(diff.Degrees) > ThresholdRotAngle / 2f)
  175. {
  176. // Ok, so this bit is difficult because ideally it would look like it's snapping to sane angles.
  177. // Otherwise PIANO DOOR STUCK! happens.
  178. // But it also needs to work with station rotation / align to the local parent.
  179. // So...
  180. var baseRotation = pulledData.WorldRotation - pulledXform.LocalRotation;
  181. var localRotation = newAngle - baseRotation;
  182. var localRotationSnapped = Angle.FromDegrees(Math.Floor((localRotation.Degrees / ThresholdRotAngle) + 0.5f) * ThresholdRotAngle);
  183. TransformSystem.SetLocalRotation(pulled, localRotationSnapped, pulledXform);
  184. }
  185. }
  186. }
  187. public override void UpdateBeforeSolve(bool prediction, float frameTime)
  188. {
  189. base.UpdateBeforeSolve(prediction, frameTime);
  190. var movingQuery = EntityQueryEnumerator<PullMovingComponent, PullableComponent, TransformComponent>();
  191. while (movingQuery.MoveNext(out var pullableEnt, out var mover, out var pullable, out var pullableXform))
  192. {
  193. if (!mover.MovingTo.IsValid(EntityManager))
  194. {
  195. RemCompDeferred<PullMovingComponent>(pullableEnt);
  196. continue;
  197. }
  198. if (pullable.Puller is not {Valid: true} puller)
  199. continue;
  200. var pullerXform = _xformQuery.Get(puller);
  201. var pullerPosition = TransformSystem.GetMapCoordinates(pullerXform);
  202. var movingTo = mover.MovingTo.ToMap(EntityManager, TransformSystem);
  203. if (movingTo.MapId != pullerPosition.MapId)
  204. {
  205. RemCompDeferred<PullMovingComponent>(pullableEnt);
  206. continue;
  207. }
  208. if (!TryComp<PhysicsComponent>(pullableEnt, out var physics) ||
  209. physics.BodyType == BodyType.Static ||
  210. movingTo.MapId != pullableXform.MapID)
  211. {
  212. RemCompDeferred<PullMovingComponent>(pullableEnt);
  213. continue;
  214. }
  215. var movingPosition = movingTo.Position;
  216. var ownerPosition = TransformSystem.GetWorldPosition(pullableXform);
  217. var diff = movingPosition - ownerPosition;
  218. var diffLength = diff.Length();
  219. if (diffLength < MaximumSettleDistance && physics.LinearVelocity.Length() < MaximumSettleVelocity)
  220. {
  221. PhysicsSystem.SetLinearVelocity(pullableEnt, Vector2.Zero, body: physics);
  222. RemCompDeferred<PullMovingComponent>(pullableEnt);
  223. continue;
  224. }
  225. var impulseModifierLerp = Math.Min(1.0f, Math.Max(0.0f, (physics.Mass - AccelModifierLowMass) / (AccelModifierHighMass - AccelModifierLowMass)));
  226. var impulseModifier = MathHelper.Lerp(AccelModifierLow, AccelModifierHigh, impulseModifierLerp);
  227. var multiplier = diffLength < 1 ? impulseModifier * diffLength : impulseModifier;
  228. // Note the implication that the real rules of physics don't apply to pulling control.
  229. var accel = diff.Normalized() * multiplier;
  230. // Now for the part where velocity gets shutdown...
  231. if (diffLength < SettleShutdownDistance && physics.LinearVelocity.Length() >= SettleMinimumShutdownVelocity)
  232. {
  233. // Shutdown velocity increases as we get closer to centre
  234. var scaling = (SettleShutdownDistance - diffLength) / SettleShutdownDistance;
  235. accel -= physics.LinearVelocity * SettleShutdownMultiplier * scaling;
  236. }
  237. PhysicsSystem.WakeBody(pullableEnt, body: physics);
  238. var impulse = accel * physics.Mass * frameTime;
  239. PhysicsSystem.ApplyLinearImpulse(pullableEnt, impulse, body: physics);
  240. // if the puller is weightless or can't move, then we apply the inverse impulse (Newton's third law).
  241. // doing it under gravity produces an unsatisfying wiggling when pulling.
  242. // If player can't move, assume they are on a chair and we need to prevent pull-moving.
  243. if (_gravity.IsWeightless(puller) && pullerXform.Comp.GridUid == null || !_actionBlockerSystem.CanMove(puller))
  244. {
  245. PhysicsSystem.WakeBody(puller);
  246. PhysicsSystem.ApplyLinearImpulse(puller, -impulse);
  247. }
  248. }
  249. }
  250. }