PullingSystem.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. using Content.Shared.ActionBlocker;
  2. using Content.Shared.Administration.Logs;
  3. using Content.Shared.Alert;
  4. using Content.Shared.Buckle.Components;
  5. using Content.Shared.Cuffs.Components;
  6. using Content.Shared.Database;
  7. using Content.Shared.Hands;
  8. using Content.Shared.Hands.EntitySystems;
  9. using Content.Shared.IdentityManagement;
  10. using Content.Shared.Input;
  11. using Content.Shared.Interaction;
  12. using Content.Shared.Item;
  13. using Content.Shared.Mobs;
  14. using Content.Shared.Mobs.Systems;
  15. using Content.Shared.Movement.Events;
  16. using Content.Shared.Movement.Pulling.Components;
  17. using Content.Shared.Movement.Pulling.Events;
  18. using Content.Shared.Movement.Systems;
  19. using Content.Shared.Popups;
  20. using Content.Shared.Pulling.Events;
  21. using Content.Shared.Standing;
  22. using Content.Shared.Verbs;
  23. using Robust.Shared.Containers;
  24. using Robust.Shared.Input.Binding;
  25. using Robust.Shared.Physics;
  26. using Robust.Shared.Physics.Components;
  27. using Robust.Shared.Physics.Events;
  28. using Robust.Shared.Physics.Systems;
  29. using Robust.Shared.Player;
  30. using Robust.Shared.Timing;
  31. namespace Content.Shared.Movement.Pulling.Systems;
  32. /// <summary>
  33. /// Allows one entity to pull another behind them via a physics distance joint.
  34. /// </summary>
  35. public sealed class PullingSystem : EntitySystem
  36. {
  37. [Dependency] private readonly IGameTiming _timing = default!;
  38. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  39. [Dependency] private readonly ActionBlockerSystem _blocker = default!;
  40. [Dependency] private readonly AlertsSystem _alertsSystem = default!;
  41. [Dependency] private readonly MovementSpeedModifierSystem _modifierSystem = default!;
  42. [Dependency] private readonly SharedJointSystem _joints = default!;
  43. [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
  44. [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
  45. [Dependency] private readonly SharedInteractionSystem _interaction = default!;
  46. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  47. [Dependency] private readonly HeldSpeedModifierSystem _clothingMoveSpeed = default!;
  48. [Dependency] private readonly SharedPopupSystem _popup = default!;
  49. public override void Initialize()
  50. {
  51. base.Initialize();
  52. UpdatesAfter.Add(typeof(SharedPhysicsSystem));
  53. UpdatesOutsidePrediction = true;
  54. SubscribeLocalEvent<PullableComponent, MoveInputEvent>(OnPullableMoveInput);
  55. SubscribeLocalEvent<PullableComponent, CollisionChangeEvent>(OnPullableCollisionChange);
  56. SubscribeLocalEvent<PullableComponent, JointRemovedEvent>(OnJointRemoved);
  57. SubscribeLocalEvent<PullableComponent, GetVerbsEvent<Verb>>(AddPullVerbs);
  58. SubscribeLocalEvent<PullableComponent, EntGotInsertedIntoContainerMessage>(OnPullableContainerInsert);
  59. SubscribeLocalEvent<PullableComponent, ModifyUncuffDurationEvent>(OnModifyUncuffDuration);
  60. SubscribeLocalEvent<PullableComponent, StopBeingPulledAlertEvent>(OnStopBeingPulledAlert);
  61. SubscribeLocalEvent<PullerComponent, UpdateMobStateEvent>(OnStateChanged);
  62. SubscribeLocalEvent<PullerComponent, AfterAutoHandleStateEvent>(OnAfterState);
  63. SubscribeLocalEvent<PullerComponent, EntGotInsertedIntoContainerMessage>(OnPullerContainerInsert);
  64. SubscribeLocalEvent<PullerComponent, EntityUnpausedEvent>(OnPullerUnpaused);
  65. SubscribeLocalEvent<PullerComponent, VirtualItemDeletedEvent>(OnVirtualItemDeleted);
  66. SubscribeLocalEvent<PullerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
  67. SubscribeLocalEvent<PullerComponent, DropHandItemsEvent>(OnDropHandItems);
  68. SubscribeLocalEvent<PullerComponent, StopPullingAlertEvent>(OnStopPullingAlert);
  69. SubscribeLocalEvent<PullableComponent, StrappedEvent>(OnBuckled);
  70. SubscribeLocalEvent<PullableComponent, BuckledEvent>(OnGotBuckled);
  71. CommandBinds.Builder
  72. .Bind(ContentKeyFunctions.ReleasePulledObject, InputCmdHandler.FromDelegate(OnReleasePulledObject, handle: false))
  73. .Register<PullingSystem>();
  74. }
  75. private void OnStateChanged(EntityUid uid, PullerComponent component, ref UpdateMobStateEvent args)
  76. {
  77. if (component.Pulling == null)
  78. return;
  79. if (TryComp<PullableComponent>(component.Pulling, out var comp) && (args.State == MobState.Critical || args.State == MobState.Dead))
  80. {
  81. TryStopPull(component.Pulling.Value, comp);
  82. }
  83. }
  84. private void OnBuckled(Entity<PullableComponent> ent, ref StrappedEvent args)
  85. {
  86. // Prevent people from pulling the entity they are buckled to
  87. if (ent.Comp.Puller == args.Buckle.Owner && !args.Buckle.Comp.PullStrap)
  88. StopPulling(ent, ent);
  89. }
  90. private void OnGotBuckled(Entity<PullableComponent> ent, ref BuckledEvent args)
  91. {
  92. StopPulling(ent, ent);
  93. }
  94. private void OnAfterState(Entity<PullerComponent> ent, ref AfterAutoHandleStateEvent args)
  95. {
  96. if (ent.Comp.Pulling == null)
  97. RemComp<ActivePullerComponent>(ent.Owner);
  98. else
  99. EnsureComp<ActivePullerComponent>(ent.Owner);
  100. }
  101. private void OnDropHandItems(EntityUid uid, PullerComponent pullerComp, DropHandItemsEvent args)
  102. {
  103. if (pullerComp.Pulling == null || pullerComp.NeedsHands)
  104. return;
  105. if (!TryComp(pullerComp.Pulling, out PullableComponent? pullableComp))
  106. return;
  107. TryStopPull(pullerComp.Pulling.Value, pullableComp, uid);
  108. }
  109. private void OnStopPullingAlert(Entity<PullerComponent> ent, ref StopPullingAlertEvent args)
  110. {
  111. if (args.Handled)
  112. return;
  113. if (!TryComp<PullableComponent>(ent.Comp.Pulling, out var pullable))
  114. return;
  115. args.Handled = TryStopPull(ent.Comp.Pulling.Value, pullable, ent);
  116. }
  117. private void OnPullerContainerInsert(Entity<PullerComponent> ent, ref EntGotInsertedIntoContainerMessage args)
  118. {
  119. if (ent.Comp.Pulling == null)
  120. return;
  121. if (!TryComp(ent.Comp.Pulling.Value, out PullableComponent? pulling))
  122. return;
  123. TryStopPull(ent.Comp.Pulling.Value, pulling, ent.Owner);
  124. }
  125. private void OnPullableContainerInsert(Entity<PullableComponent> ent, ref EntGotInsertedIntoContainerMessage args)
  126. {
  127. TryStopPull(ent.Owner, ent.Comp);
  128. }
  129. private void OnModifyUncuffDuration(Entity<PullableComponent> ent, ref ModifyUncuffDurationEvent args)
  130. {
  131. if (!ent.Comp.BeingPulled)
  132. return;
  133. // We don't care if the person is being uncuffed by someone else
  134. if (args.User != args.Target)
  135. return;
  136. args.Duration *= 2;
  137. }
  138. private void OnStopBeingPulledAlert(Entity<PullableComponent> ent, ref StopBeingPulledAlertEvent args)
  139. {
  140. if (args.Handled)
  141. return;
  142. args.Handled = TryStopPull(ent, ent, ent);
  143. }
  144. public override void Shutdown()
  145. {
  146. base.Shutdown();
  147. CommandBinds.Unregister<PullingSystem>();
  148. }
  149. private void OnPullerUnpaused(EntityUid uid, PullerComponent component, ref EntityUnpausedEvent args)
  150. {
  151. component.NextThrow += args.PausedTime;
  152. }
  153. private void OnVirtualItemDeleted(EntityUid uid, PullerComponent component, VirtualItemDeletedEvent args)
  154. {
  155. // If client deletes the virtual hand then stop the pull.
  156. if (component.Pulling == null)
  157. return;
  158. if (component.Pulling != args.BlockingEntity)
  159. return;
  160. if (EntityManager.TryGetComponent(args.BlockingEntity, out PullableComponent? comp))
  161. {
  162. TryStopPull(args.BlockingEntity, comp);
  163. }
  164. }
  165. private void AddPullVerbs(EntityUid uid, PullableComponent component, GetVerbsEvent<Verb> args)
  166. {
  167. if (!args.CanAccess || !args.CanInteract)
  168. return;
  169. // Are they trying to pull themselves up by their bootstraps?
  170. if (args.User == args.Target)
  171. return;
  172. //TODO VERB ICONS add pulling icon
  173. if (component.Puller == args.User)
  174. {
  175. Verb verb = new()
  176. {
  177. Text = Loc.GetString("pulling-verb-get-data-text-stop-pulling"),
  178. Act = () => TryStopPull(uid, component, user: args.User),
  179. DoContactInteraction = false // pulling handle its own contact interaction.
  180. };
  181. args.Verbs.Add(verb);
  182. }
  183. else if (CanPull(args.User, args.Target))
  184. {
  185. Verb verb = new()
  186. {
  187. Text = Loc.GetString("pulling-verb-get-data-text"),
  188. Act = () => TryStartPull(args.User, args.Target),
  189. DoContactInteraction = false // pulling handle its own contact interaction.
  190. };
  191. args.Verbs.Add(verb);
  192. }
  193. }
  194. private void OnRefreshMovespeed(EntityUid uid, PullerComponent component, RefreshMovementSpeedModifiersEvent args)
  195. {
  196. if (TryComp<HeldSpeedModifierComponent>(component.Pulling, out var heldMoveSpeed) && component.Pulling.HasValue)
  197. {
  198. var (walkMod, sprintMod) =
  199. _clothingMoveSpeed.GetHeldMovementSpeedModifiers(component.Pulling.Value, heldMoveSpeed);
  200. args.ModifySpeed(walkMod, sprintMod);
  201. return;
  202. }
  203. args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier);
  204. }
  205. private void OnPullableMoveInput(EntityUid uid, PullableComponent component, ref MoveInputEvent args)
  206. {
  207. // If someone moves then break their pulling.
  208. if (!component.BeingPulled)
  209. return;
  210. var entity = args.Entity;
  211. if (!_blocker.CanMove(entity))
  212. return;
  213. TryStopPull(uid, component, user: uid);
  214. }
  215. private void OnPullableCollisionChange(EntityUid uid, PullableComponent component, ref CollisionChangeEvent args)
  216. {
  217. // IDK what this is supposed to be.
  218. if (!_timing.ApplyingState && component.PullJointId != null && !args.CanCollide)
  219. {
  220. _joints.RemoveJoint(uid, component.PullJointId);
  221. }
  222. }
  223. private void OnJointRemoved(EntityUid uid, PullableComponent component, JointRemovedEvent args)
  224. {
  225. // Just handles the joint getting nuked without going through pulling system (valid behavior).
  226. // Not relevant / pullable state handle it.
  227. if (component.Puller != args.OtherEntity ||
  228. args.Joint.ID != component.PullJointId ||
  229. _timing.ApplyingState)
  230. {
  231. return;
  232. }
  233. if (args.Joint.ID != component.PullJointId || component.Puller == null)
  234. return;
  235. StopPulling(uid, component);
  236. }
  237. /// <summary>
  238. /// Forces pulling to stop and handles cleanup.
  239. /// </summary>
  240. private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp)
  241. {
  242. if (pullableComp.Puller == null)
  243. return;
  244. if (!_timing.ApplyingState)
  245. {
  246. // Joint shutdown
  247. if (pullableComp.PullJointId != null)
  248. {
  249. _joints.RemoveJoint(pullableUid, pullableComp.PullJointId);
  250. pullableComp.PullJointId = null;
  251. }
  252. if (TryComp<PhysicsComponent>(pullableUid, out var pullablePhysics))
  253. {
  254. _physics.SetFixedRotation(pullableUid, pullableComp.PrevFixedRotation, body: pullablePhysics);
  255. }
  256. }
  257. var oldPuller = pullableComp.Puller;
  258. if (oldPuller != null)
  259. RemComp<ActivePullerComponent>(oldPuller.Value);
  260. pullableComp.PullJointId = null;
  261. pullableComp.Puller = null;
  262. Dirty(pullableUid, pullableComp);
  263. // No more joints with puller -> force stop pull.
  264. if (TryComp<PullerComponent>(oldPuller, out var pullerComp))
  265. {
  266. var pullerUid = oldPuller.Value;
  267. _alertsSystem.ClearAlert(pullerUid, pullerComp.PullingAlert);
  268. pullerComp.Pulling = null;
  269. Dirty(oldPuller.Value, pullerComp);
  270. // Messaging
  271. var message = new PullStoppedMessage(pullerUid, pullableUid);
  272. _modifierSystem.RefreshMovementSpeedModifiers(pullerUid);
  273. _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(pullerUid):user} stopped pulling {ToPrettyString(pullableUid):target}");
  274. RaiseLocalEvent(pullerUid, message);
  275. RaiseLocalEvent(pullableUid, message);
  276. }
  277. _alertsSystem.ClearAlert(pullableUid, pullableComp.PulledAlert);
  278. }
  279. public bool IsPulled(EntityUid uid, PullableComponent? component = null)
  280. {
  281. return Resolve(uid, ref component, false) && component.BeingPulled;
  282. }
  283. public bool IsPulling(EntityUid puller, PullerComponent? component = null)
  284. {
  285. return Resolve(puller, ref component, false) && component.Pulling != null;
  286. }
  287. private void OnReleasePulledObject(ICommonSession? session)
  288. {
  289. if (session?.AttachedEntity is not { Valid: true } player)
  290. {
  291. return;
  292. }
  293. if (!TryComp(player, out PullerComponent? pullerComp) ||
  294. !TryComp(pullerComp.Pulling, out PullableComponent? pullableComp))
  295. {
  296. return;
  297. }
  298. TryStopPull(pullerComp.Pulling.Value, pullableComp, user: player);
  299. }
  300. public bool CanPull(EntityUid puller, EntityUid pullableUid, PullerComponent? pullerComp = null)
  301. {
  302. if (!Resolve(puller, ref pullerComp, false))
  303. {
  304. return false;
  305. }
  306. if (pullerComp.NeedsHands
  307. && !_handsSystem.TryGetEmptyHand(puller, out _)
  308. && pullerComp.Pulling == null)
  309. {
  310. return false;
  311. }
  312. if (!_blocker.CanInteract(puller, pullableUid))
  313. {
  314. return false;
  315. }
  316. if (!EntityManager.TryGetComponent<PhysicsComponent>(pullableUid, out var physics))
  317. {
  318. return false;
  319. }
  320. if (physics.BodyType == BodyType.Static)
  321. {
  322. return false;
  323. }
  324. if (puller == pullableUid)
  325. {
  326. return false;
  327. }
  328. if (!_containerSystem.IsInSameOrNoContainer(puller, pullableUid))
  329. {
  330. return false;
  331. }
  332. var getPulled = new BeingPulledAttemptEvent(puller, pullableUid);
  333. RaiseLocalEvent(pullableUid, getPulled, true);
  334. var startPull = new StartPullAttemptEvent(puller, pullableUid);
  335. RaiseLocalEvent(puller, startPull, true);
  336. return !startPull.Cancelled && !getPulled.Cancelled;
  337. }
  338. public bool TogglePull(Entity<PullableComponent?> pullable, EntityUid pullerUid)
  339. {
  340. if (!Resolve(pullable, ref pullable.Comp, false))
  341. return false;
  342. if (pullable.Comp.Puller == pullerUid)
  343. {
  344. return TryStopPull(pullable, pullable.Comp);
  345. }
  346. return TryStartPull(pullerUid, pullable, pullableComp: pullable);
  347. }
  348. public bool TogglePull(EntityUid pullerUid, PullerComponent puller)
  349. {
  350. if (!TryComp<PullableComponent>(puller.Pulling, out var pullable))
  351. return false;
  352. return TogglePull((puller.Pulling.Value, pullable), pullerUid);
  353. }
  354. public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid,
  355. PullerComponent? pullerComp = null, PullableComponent? pullableComp = null)
  356. {
  357. if (!Resolve(pullerUid, ref pullerComp, false) ||
  358. !Resolve(pullableUid, ref pullableComp, false))
  359. {
  360. return false;
  361. }
  362. if (pullerComp.Pulling == pullableUid)
  363. return true;
  364. if (!CanPull(pullerUid, pullableUid))
  365. return false;
  366. if (!TryComp(pullerUid, out PhysicsComponent? pullerPhysics) || !TryComp(pullableUid, out PhysicsComponent? pullablePhysics))
  367. return false;
  368. // Ensure that the puller is not currently pulling anything.
  369. if (TryComp<PullableComponent>(pullerComp.Pulling, out var oldPullable)
  370. && !TryStopPull(pullerComp.Pulling.Value, oldPullable, pullerUid))
  371. return false;
  372. // Stop anyone else pulling the entity we want to pull
  373. if (pullableComp.Puller != null)
  374. {
  375. // We're already pulling this item
  376. if (pullableComp.Puller == pullerUid)
  377. return false;
  378. if (!TryStopPull(pullableUid, pullableComp, pullableComp.Puller))
  379. return false;
  380. }
  381. var pullAttempt = new PullAttemptEvent(pullerUid, pullableUid);
  382. RaiseLocalEvent(pullerUid, pullAttempt);
  383. if (pullAttempt.Cancelled)
  384. return false;
  385. RaiseLocalEvent(pullableUid, pullAttempt);
  386. if (pullAttempt.Cancelled)
  387. return false;
  388. // Pulling confirmed
  389. _interaction.DoContactInteraction(pullableUid, pullerUid);
  390. // Use net entity so it's consistent across client and server.
  391. pullableComp.PullJointId = $"pull-joint-{GetNetEntity(pullableUid)}";
  392. EnsureComp<ActivePullerComponent>(pullerUid);
  393. pullerComp.Pulling = pullableUid;
  394. pullableComp.Puller = pullerUid;
  395. // store the pulled entity's physics FixedRotation setting in case we change it
  396. pullableComp.PrevFixedRotation = pullablePhysics.FixedRotation;
  397. // joint state handling will manage its own state
  398. if (!_timing.ApplyingState)
  399. {
  400. var joint = _joints.CreateDistanceJoint(pullableUid, pullerUid,
  401. pullablePhysics.LocalCenter, pullerPhysics.LocalCenter,
  402. id: pullableComp.PullJointId);
  403. joint.CollideConnected = false;
  404. // This maximum has to be there because if the object is constrained too closely, the clamping goes backwards and asserts.
  405. // Internally, the joint length has been set to the distance between the pivots.
  406. // Add an additional 15cm (pretty arbitrary) to the maximum length for the hard limit.
  407. joint.MaxLength = joint.Length + 0.15f;
  408. joint.MinLength = 0f;
  409. // Set the spring stiffness to zero. The joint won't have any effect provided
  410. // the current length is beteen MinLength and MaxLength. At those limits, the
  411. // joint will have infinite stiffness.
  412. joint.Stiffness = 0f;
  413. _physics.SetFixedRotation(pullableUid, pullableComp.FixedRotationOnPull, body: pullablePhysics);
  414. }
  415. // Messaging
  416. var message = new PullStartedMessage(pullerUid, pullableUid);
  417. _modifierSystem.RefreshMovementSpeedModifiers(pullerUid);
  418. _alertsSystem.ShowAlert(pullerUid, pullerComp.PullingAlert);
  419. _alertsSystem.ShowAlert(pullableUid, pullableComp.PulledAlert);
  420. RaiseLocalEvent(pullerUid, message);
  421. RaiseLocalEvent(pullableUid, message);
  422. Dirty(pullerUid, pullerComp);
  423. Dirty(pullableUid, pullableComp);
  424. var pullingMessage =
  425. Loc.GetString("getting-pulled-popup", ("puller", Identity.Entity(pullerUid, EntityManager)));
  426. _popup.PopupEntity(pullingMessage, pullableUid, pullableUid);
  427. _adminLogger.Add(LogType.Action, LogImpact.Low,
  428. $"{ToPrettyString(pullerUid):user} started pulling {ToPrettyString(pullableUid):target}");
  429. return true;
  430. }
  431. public bool TryStopPull(EntityUid pullableUid, PullableComponent pullable, EntityUid? user = null)
  432. {
  433. var pullerUidNull = pullable.Puller;
  434. if (pullerUidNull == null)
  435. return true;
  436. if (user != null && !_blocker.CanInteract(user.Value, pullableUid))
  437. return false;
  438. var msg = new AttemptStopPullingEvent(user);
  439. RaiseLocalEvent(pullableUid, msg, true);
  440. if (msg.Cancelled)
  441. return false;
  442. StopPulling(pullableUid, pullable);
  443. return true;
  444. }
  445. }