DragDropSystem.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. using System.Numerics;
  2. using Content.Client.CombatMode;
  3. using Content.Client.Gameplay;
  4. using Content.Client.Outline;
  5. using Content.Shared.ActionBlocker;
  6. using Content.Shared.CCVar;
  7. using Content.Shared.DragDrop;
  8. using Content.Shared.Interaction;
  9. using Content.Shared.Interaction.Events;
  10. using Content.Shared.Popups;
  11. using Robust.Client.GameObjects;
  12. using Robust.Client.Graphics;
  13. using Robust.Client.Input;
  14. using Robust.Client.Player;
  15. using Robust.Client.State;
  16. using Robust.Shared.Configuration;
  17. using Robust.Shared.Input;
  18. using Robust.Shared.Input.Binding;
  19. using Robust.Shared.Map;
  20. using Robust.Shared.Player;
  21. using Robust.Shared.Prototypes;
  22. using Robust.Shared.Utility;
  23. using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
  24. namespace Content.Client.Interaction;
  25. /// <summary>
  26. /// Handles clientside drag and drop logic
  27. /// </summary>
  28. public sealed class DragDropSystem : SharedDragDropSystem
  29. {
  30. [Dependency] private readonly IStateManager _stateManager = default!;
  31. [Dependency] private readonly IInputManager _inputManager = default!;
  32. [Dependency] private readonly IEyeManager _eyeManager = default!;
  33. [Dependency] private readonly IPlayerManager _playerManager = default!;
  34. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  35. [Dependency] private readonly IConfigurationManager _cfgMan = default!;
  36. [Dependency] private readonly InteractionOutlineSystem _outline = default!;
  37. [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
  38. [Dependency] private readonly CombatModeSystem _combatMode = default!;
  39. [Dependency] private readonly InputSystem _inputSystem = default!;
  40. [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
  41. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  42. [Dependency] private readonly SharedPopupSystem _popup = default!;
  43. [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
  44. // how often to recheck possible targets (prevents calling expensive
  45. // check logic each update)
  46. private const float TargetRecheckInterval = 0.25f;
  47. // if a drag ends up being cancelled and it has been under this
  48. // amount of time since the mousedown, we will "replay" the original
  49. // mousedown event so it can be treated like a regular click
  50. private const float MaxMouseDownTimeForReplayingClick = 0.85f;
  51. [ValidatePrototypeId<ShaderPrototype>]
  52. private const string ShaderDropTargetInRange = "SelectionOutlineInrange";
  53. [ValidatePrototypeId<ShaderPrototype>]
  54. private const string ShaderDropTargetOutOfRange = "SelectionOutline";
  55. /// <summary>
  56. /// Current entity being dragged around.
  57. /// </summary>
  58. private EntityUid? _draggedEntity;
  59. /// <summary>
  60. /// If an entity is being dragged is there a drag shadow.
  61. /// </summary>
  62. private EntityUid? _dragShadow;
  63. /// <summary>
  64. /// Time since mouse down over the dragged entity
  65. /// </summary>
  66. private float _mouseDownTime;
  67. /// <summary>
  68. /// how much time since last recheck of all possible targets
  69. /// </summary>
  70. private float _targetRecheckTime;
  71. /// <summary>
  72. /// Reserved initial mousedown event so we can replay it if no drag ends up being performed
  73. /// </summary>
  74. private PointerInputCmdHandler.PointerInputCmdArgs? _savedMouseDown;
  75. /// <summary>
  76. /// Whether we are currently replaying the original mouse down, so we
  77. /// can ignore any events sent to this system
  78. /// </summary>
  79. private bool _isReplaying;
  80. public float Deadzone;
  81. private DragState _state = DragState.NotDragging;
  82. /// <summary>
  83. /// screen pos where the mouse down began for the drag
  84. /// </summary>
  85. private ScreenCoordinates? _mouseDownScreenPos;
  86. private ShaderInstance? _dropTargetInRangeShader;
  87. private ShaderInstance? _dropTargetOutOfRangeShader;
  88. private readonly List<SpriteComponent> _highlightedSprites = new();
  89. public override void Initialize()
  90. {
  91. base.Initialize();
  92. UpdatesOutsidePrediction = true;
  93. UpdatesAfter.Add(typeof(SharedEyeSystem));
  94. Subs.CVar(_cfgMan, CCVars.DragDropDeadZone, SetDeadZone, true);
  95. _dropTargetInRangeShader = _prototypeManager.Index<ShaderPrototype>(ShaderDropTargetInRange).Instance();
  96. _dropTargetOutOfRangeShader = _prototypeManager.Index<ShaderPrototype>(ShaderDropTargetOutOfRange).Instance();
  97. // needs to fire on mouseup and mousedown so we can detect a drag / drop
  98. CommandBinds.Builder
  99. .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(OnUse, false, true), new[] { typeof(SharedInteractionSystem) })
  100. .Register<DragDropSystem>();
  101. }
  102. private void SetDeadZone(float deadZone)
  103. {
  104. Deadzone = deadZone;
  105. }
  106. public override void Shutdown()
  107. {
  108. CommandBinds.Unregister<DragDropSystem>();
  109. base.Shutdown();
  110. }
  111. private bool OnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
  112. {
  113. // not currently predicted
  114. if (_inputSystem.Predicted)
  115. return false;
  116. // currently replaying a saved click, don't handle this because
  117. // we already decided this click doesn't represent an actual drag attempt
  118. if (_isReplaying)
  119. return false;
  120. if (args.State == BoundKeyState.Down)
  121. {
  122. return OnUseMouseDown(args);
  123. }
  124. if (args.State == BoundKeyState.Up)
  125. {
  126. return OnUseMouseUp(args);
  127. }
  128. return false;
  129. }
  130. private void EndDrag()
  131. {
  132. if (_state == DragState.NotDragging)
  133. return;
  134. if (_dragShadow != null)
  135. {
  136. Del(_dragShadow.Value);
  137. _dragShadow = null;
  138. }
  139. _draggedEntity = null;
  140. _state = DragState.NotDragging;
  141. _mouseDownScreenPos = null;
  142. RemoveHighlights();
  143. _outline.SetEnabled(true);
  144. _mouseDownTime = 0;
  145. _savedMouseDown = null;
  146. }
  147. private bool OnUseMouseDown(in PointerInputCmdHandler.PointerInputCmdArgs args)
  148. {
  149. if (args.Session?.AttachedEntity is not {Valid: true} dragger ||
  150. _combatMode.IsInCombatMode())
  151. {
  152. return false;
  153. }
  154. // cancel any current dragging if there is one (shouldn't be because they would've had to have lifted
  155. // the mouse, canceling the drag, but just being cautious)
  156. EndDrag();
  157. var entity = args.EntityUid;
  158. // possibly initiating a drag
  159. // check if the clicked entity is draggable
  160. if (!Exists(entity))
  161. {
  162. return false;
  163. }
  164. // check if the entity is reachable
  165. if (!_interactionSystem.InRangeUnobstructed(dragger, entity))
  166. {
  167. return false;
  168. }
  169. var ev = new CanDragEvent();
  170. RaiseLocalEvent(entity, ref ev);
  171. if (ev.Handled != true)
  172. return false;
  173. _draggedEntity = entity;
  174. _state = DragState.MouseDown;
  175. _mouseDownScreenPos = args.ScreenCoordinates;
  176. _mouseDownTime = 0;
  177. // don't want anything else to process the click,
  178. // but we will save the event so we can "re-play" it if this drag does
  179. // not turn into an actual drag so the click can be handled normally
  180. _savedMouseDown = args;
  181. return true;
  182. }
  183. private void StartDrag()
  184. {
  185. if (!Exists(_draggedEntity))
  186. {
  187. // something happened to the clicked entity or we moved the mouse off the target so
  188. // we shouldn't replay the original click
  189. return;
  190. }
  191. _state = DragState.Dragging;
  192. DebugTools.Assert(_dragShadow == null);
  193. _outline.SetEnabled(false);
  194. HighlightTargets();
  195. if (TryComp<SpriteComponent>(_draggedEntity, out var draggedSprite))
  196. {
  197. var screenPos = _inputManager.MouseScreenPosition;
  198. // No _draggedEntity in null window (Happens in tests)
  199. if (!screenPos.IsValid)
  200. return;
  201. // pop up drag shadow under mouse
  202. var mousePos = _eyeManager.PixelToMap(screenPos);
  203. _dragShadow = EntityManager.SpawnEntity("dragshadow", mousePos);
  204. var dragSprite = Comp<SpriteComponent>(_dragShadow.Value);
  205. dragSprite.CopyFrom(draggedSprite);
  206. dragSprite.RenderOrder = EntityManager.CurrentTick.Value;
  207. dragSprite.Color = dragSprite.Color.WithAlpha(0.7f);
  208. // keep it on top of everything
  209. dragSprite.DrawDepth = (int) DrawDepth.Overlays;
  210. if (!dragSprite.NoRotation)
  211. {
  212. _transformSystem.SetWorldRotationNoLerp(_dragShadow.Value, _transformSystem.GetWorldRotation(_draggedEntity.Value));
  213. }
  214. // drag initiated
  215. return;
  216. }
  217. Log.Warning($"Unable to display drag shadow for {ToPrettyString(_draggedEntity.Value)} because it has no sprite component.");
  218. }
  219. private bool UpdateDrag(float frameTime)
  220. {
  221. if (!Exists(_draggedEntity) || _combatMode.IsInCombatMode())
  222. {
  223. EndDrag();
  224. return false;
  225. }
  226. var player = _playerManager.LocalEntity;
  227. // still in range of the thing we are dragging?
  228. if (player == null || !_interactionSystem.InRangeUnobstructed(player.Value, _draggedEntity.Value))
  229. {
  230. return false;
  231. }
  232. if (_dragShadow == null)
  233. return false;
  234. _targetRecheckTime += frameTime;
  235. if (_targetRecheckTime > TargetRecheckInterval)
  236. {
  237. HighlightTargets();
  238. _targetRecheckTime -= TargetRecheckInterval;
  239. }
  240. return true;
  241. }
  242. private bool OnUseMouseUp(in PointerInputCmdHandler.PointerInputCmdArgs args)
  243. {
  244. if (_state == DragState.MouseDown)
  245. {
  246. // haven't started the drag yet, quick mouseup, definitely treat it as a normal click by
  247. // replaying the original cmd
  248. try
  249. {
  250. if (_savedMouseDown.HasValue && _mouseDownTime < MaxMouseDownTimeForReplayingClick)
  251. {
  252. var savedValue = _savedMouseDown.Value;
  253. _isReplaying = true;
  254. // adjust the timing info based on the current tick so it appears as if it happened now
  255. var replayMsg = savedValue.OriginalMessage;
  256. switch (replayMsg)
  257. {
  258. case ClientFullInputCmdMessage clientInput:
  259. replayMsg = new ClientFullInputCmdMessage(args.OriginalMessage.Tick,
  260. args.OriginalMessage.SubTick,
  261. replayMsg.InputFunctionId)
  262. {
  263. State = replayMsg.State,
  264. Coordinates = clientInput.Coordinates,
  265. ScreenCoordinates = clientInput.ScreenCoordinates,
  266. Uid = clientInput.Uid,
  267. };
  268. break;
  269. case FullInputCmdMessage fullInput:
  270. replayMsg = new FullInputCmdMessage(args.OriginalMessage.Tick,
  271. args.OriginalMessage.SubTick,
  272. replayMsg.InputFunctionId, replayMsg.State, fullInput.Coordinates, fullInput.ScreenCoordinates,
  273. fullInput.Uid);
  274. break;
  275. default:
  276. throw new ArgumentOutOfRangeException();
  277. }
  278. if (savedValue.Session != null)
  279. {
  280. _inputSystem.HandleInputCommand(savedValue.Session, EngineKeyFunctions.Use, replayMsg,
  281. true);
  282. }
  283. _isReplaying = false;
  284. }
  285. }
  286. finally
  287. {
  288. EndDrag();
  289. }
  290. return false;
  291. }
  292. var localPlayer = _playerManager.LocalEntity;
  293. if (localPlayer == null || !Exists(_draggedEntity))
  294. {
  295. EndDrag();
  296. return false;
  297. }
  298. IEnumerable<EntityUid> entities;
  299. var coords = args.Coordinates;
  300. if (_stateManager.CurrentState is GameplayState screen)
  301. {
  302. entities = screen.GetClickableEntities(coords);
  303. }
  304. else
  305. {
  306. entities = Array.Empty<EntityUid>();
  307. }
  308. var outOfRange = false;
  309. var user = localPlayer.Value;
  310. foreach (var entity in entities)
  311. {
  312. if (entity == _draggedEntity)
  313. continue;
  314. // check if it's able to be dropped on by current dragged entity
  315. var valid = ValidDragDrop(user, _draggedEntity.Value, entity);
  316. if (valid != true) continue;
  317. if (!_interactionSystem.InRangeUnobstructed(user, entity)
  318. || !_interactionSystem.InRangeUnobstructed(user, _draggedEntity.Value))
  319. {
  320. outOfRange = true;
  321. continue;
  322. }
  323. // tell the server about the drop attempt
  324. RaisePredictiveEvent(new DragDropRequestEvent(GetNetEntity(_draggedEntity.Value), GetNetEntity(entity)));
  325. EndDrag();
  326. return true;
  327. }
  328. if (outOfRange)
  329. {
  330. _popup.PopupEntity(Loc.GetString("drag-drop-system-out-of-range-text"), _draggedEntity.Value, Filter.Local(), true);
  331. }
  332. EndDrag();
  333. return false;
  334. }
  335. // TODO make this just use TargetOutlineSystem
  336. private void HighlightTargets()
  337. {
  338. if (!Exists(_draggedEntity) ||
  339. !Exists(_dragShadow))
  340. {
  341. return;
  342. }
  343. var user = _playerManager.LocalEntity;
  344. if (user == null)
  345. return;
  346. // highlights the possible targets which are visible
  347. // and able to be dropped on by the current dragged entity
  348. // remove current highlights
  349. RemoveHighlights();
  350. // find possible targets on screen even if not reachable
  351. // TODO: Duplicated in SpriteSystem and TargetOutlineSystem. Should probably be cached somewhere for a frame?
  352. var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
  353. var expansion = new Vector2(1.5f, 1.5f);
  354. var bounds = new Box2(mousePos.Position - expansion, mousePos.Position + expansion);
  355. var pvsEntities = _lookup.GetEntitiesIntersecting(mousePos.MapId, bounds);
  356. var spriteQuery = GetEntityQuery<SpriteComponent>();
  357. foreach (var entity in pvsEntities)
  358. {
  359. if (!spriteQuery.TryGetComponent(entity, out var inRangeSprite) ||
  360. !inRangeSprite.Visible ||
  361. entity == _draggedEntity)
  362. {
  363. continue;
  364. }
  365. var valid = ValidDragDrop(user.Value, _draggedEntity.Value, entity);
  366. // check if it's able to be dropped on by current dragged entity
  367. if (valid == null)
  368. continue;
  369. // We'll do a final check given server-side does this before any dragdrop can take place.
  370. if (valid.Value)
  371. {
  372. valid = _interactionSystem.InRangeUnobstructed(user.Value, _draggedEntity.Value)
  373. && _interactionSystem.InRangeUnobstructed(user.Value, entity);
  374. }
  375. if (inRangeSprite.PostShader != null &&
  376. inRangeSprite.PostShader != _dropTargetInRangeShader &&
  377. inRangeSprite.PostShader != _dropTargetOutOfRangeShader)
  378. {
  379. continue;
  380. }
  381. // highlight depending on whether its in or out of range
  382. inRangeSprite.PostShader = valid.Value ? _dropTargetInRangeShader : _dropTargetOutOfRangeShader;
  383. inRangeSprite.RenderOrder = EntityManager.CurrentTick.Value;
  384. _highlightedSprites.Add(inRangeSprite);
  385. }
  386. }
  387. private void RemoveHighlights()
  388. {
  389. foreach (var highlightedSprite in _highlightedSprites)
  390. {
  391. if (highlightedSprite.PostShader != _dropTargetInRangeShader && highlightedSprite.PostShader != _dropTargetOutOfRangeShader)
  392. continue;
  393. highlightedSprite.PostShader = null;
  394. highlightedSprite.RenderOrder = 0;
  395. }
  396. _highlightedSprites.Clear();
  397. }
  398. /// <summary>
  399. /// Are these args valid for drag-drop?
  400. /// </summary>
  401. /// <returns>
  402. /// Returns null if no interactions are available or the user / target cannot interact with each other.
  403. /// Returns false if interactions exist but are not available currently.
  404. /// </returns>
  405. private bool? ValidDragDrop(EntityUid user, EntityUid dragged, EntityUid target)
  406. {
  407. if (!_actionBlockerSystem.CanInteract(user, target))
  408. return null;
  409. // CanInteract() doesn't support checking a second "target" entity.
  410. // Doing so manually:
  411. var ev = new GettingInteractedWithAttemptEvent(user, dragged);
  412. RaiseLocalEvent(dragged, ref ev);
  413. if (ev.Cancelled)
  414. return false;
  415. var dropEv = new CanDropDraggedEvent(user, target);
  416. RaiseLocalEvent(dragged, ref dropEv);
  417. if (dropEv.Handled)
  418. {
  419. if (!dropEv.CanDrop)
  420. return false;
  421. }
  422. var dropEv2 = new CanDropTargetEvent(user, dragged);
  423. RaiseLocalEvent(target, ref dropEv2);
  424. if (dropEv2.Handled)
  425. return dropEv2.CanDrop;
  426. if (dropEv.Handled && dropEv.CanDrop)
  427. return true;
  428. return null;
  429. }
  430. public override void Update(float frameTime)
  431. {
  432. base.Update(frameTime);
  433. switch (_state)
  434. {
  435. // check if dragging should begin
  436. case DragState.MouseDown:
  437. {
  438. var screenPos = _inputManager.MouseScreenPosition;
  439. if ((_mouseDownScreenPos!.Value.Position - screenPos.Position).Length() > Deadzone)
  440. {
  441. StartDrag();
  442. }
  443. break;
  444. }
  445. case DragState.Dragging:
  446. UpdateDrag(frameTime);
  447. break;
  448. }
  449. }
  450. public override void FrameUpdate(float frameTime)
  451. {
  452. base.FrameUpdate(frameTime);
  453. // Update position every frame to make it smooth.
  454. if (Exists(_dragShadow))
  455. {
  456. var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
  457. _transformSystem.SetWorldPosition(_dragShadow.Value, mousePos.Position);
  458. }
  459. }
  460. }
  461. public enum DragState : byte
  462. {
  463. NotDragging,
  464. /// <summary>
  465. /// Not dragging yet, waiting to see
  466. /// if they hold for long enough
  467. /// </summary>
  468. MouseDown,
  469. /// <summary>
  470. /// Currently dragging something
  471. /// </summary>
  472. Dragging,
  473. }