1
0

TabletopSystem.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. using System.Numerics;
  2. using Content.Client.Tabletop.UI;
  3. using Content.Client.Viewport;
  4. using Content.Shared.Tabletop;
  5. using Content.Shared.Tabletop.Components;
  6. using Content.Shared.Tabletop.Events;
  7. using JetBrains.Annotations;
  8. using Robust.Client.GameObjects;
  9. using Robust.Client.Graphics;
  10. using Robust.Client.Input;
  11. using Robust.Client.Player;
  12. using Robust.Client.UserInterface;
  13. using Robust.Client.UserInterface.CustomControls;
  14. using Robust.Shared.Input;
  15. using Robust.Shared.Input.Binding;
  16. using Robust.Shared.Map;
  17. using Robust.Shared.Timing;
  18. using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
  19. namespace Content.Client.Tabletop
  20. {
  21. [UsedImplicitly]
  22. public sealed class TabletopSystem : SharedTabletopSystem
  23. {
  24. [Dependency] private readonly IInputManager _inputManager = default!;
  25. [Dependency] private readonly IUserInterfaceManager _uiManger = default!;
  26. [Dependency] private readonly IPlayerManager _playerManager = default!;
  27. [Dependency] private readonly IGameTiming _gameTiming = default!;
  28. [Dependency] private readonly AppearanceSystem _appearance = default!;
  29. [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
  30. // Time in seconds to wait until sending the location of a dragged entity to the server again
  31. private const float Delay = 1f / 10; // 10 Hz
  32. private float _timePassed; // Time passed since last update sent to the server.
  33. private EntityUid? _draggedEntity; // Entity being dragged
  34. private ScalingViewport? _viewport; // Viewport currently being used
  35. private DefaultWindow? _window; // Current open tabletop window (only allow one at a time)
  36. private EntityUid? _table; // The table entity of the currently open game session
  37. public override void Initialize()
  38. {
  39. base.Initialize();
  40. UpdatesOutsidePrediction = true;
  41. CommandBinds.Builder
  42. .Bind(EngineKeyFunctions.Use, new PointerInputCmdHandler(OnUse, false, true))
  43. .Bind(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnUseSecondary, true, true))
  44. .Register<TabletopSystem>();
  45. SubscribeNetworkEvent<TabletopPlayEvent>(OnTabletopPlay);
  46. SubscribeLocalEvent<TabletopDraggableComponent, ComponentRemove>(HandleDraggableRemoved);
  47. SubscribeLocalEvent<TabletopDraggableComponent, AppearanceChangeEvent>(OnAppearanceChange);
  48. }
  49. private void HandleDraggableRemoved(EntityUid uid, TabletopDraggableComponent component, ComponentRemove args)
  50. {
  51. if (_draggedEntity == uid)
  52. StopDragging(false);
  53. }
  54. public override void FrameUpdate(float frameTime)
  55. {
  56. if (_window == null)
  57. return;
  58. // If there is no player entity, return
  59. if (_playerManager.LocalEntity is not { } playerEntity)
  60. return;
  61. if (!CanSeeTable(playerEntity, _table))
  62. {
  63. StopDragging();
  64. _window?.Close();
  65. return;
  66. }
  67. // If no entity is being dragged or no viewport is clicked, return
  68. if (_draggedEntity == null || _viewport == null) return;
  69. if (!CanDrag(playerEntity, _draggedEntity.Value, out var draggableComponent))
  70. {
  71. StopDragging();
  72. return;
  73. }
  74. // If the dragged entity has another dragging player, drop the item
  75. // This should happen if the local player is dragging an item, and another player grabs it out of their hand
  76. if (draggableComponent.DraggingPlayer != null &&
  77. draggableComponent.DraggingPlayer != _playerManager.LocalSession!.UserId)
  78. {
  79. StopDragging(false);
  80. return;
  81. }
  82. // Map mouse position to EntityCoordinates
  83. var coords = _viewport.PixelToMap(_inputManager.MouseScreenPosition.Position);
  84. // Clamp coordinates to viewport
  85. var clampedCoords = ClampPositionToViewport(coords, _viewport);
  86. if (clampedCoords.Equals(MapCoordinates.Nullspace)) return;
  87. // Move the entity locally every update
  88. _transformSystem.SetWorldPosition(_draggedEntity.Value, clampedCoords.Position);
  89. // Increment total time passed
  90. _timePassed += frameTime;
  91. // Only send new position to server when Delay is reached
  92. if (_timePassed >= Delay && _table != null)
  93. {
  94. RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), clampedCoords, GetNetEntity(_table.Value)));
  95. _timePassed -= Delay;
  96. }
  97. }
  98. #region Event handlers
  99. /// <summary>
  100. /// Runs when the player presses the "Play Game" verb on a tabletop game.
  101. /// Opens a viewport where they can then play the game.
  102. /// </summary>
  103. private void OnTabletopPlay(TabletopPlayEvent msg)
  104. {
  105. // Close the currently opened window, if it exists
  106. _window?.Close();
  107. _table = GetEntity(msg.TableUid);
  108. // Get the camera entity that the server has created for us
  109. var camera = GetEntity(msg.CameraUid);
  110. if (!EntityManager.TryGetComponent<EyeComponent>(camera, out var eyeComponent))
  111. {
  112. // If there is no eye, print error and do not open any window
  113. Log.Error("Camera entity does not have eye component!");
  114. return;
  115. }
  116. // Create a window to contain the viewport
  117. _window = new TabletopWindow(eyeComponent.Eye, (msg.Size.X, msg.Size.Y))
  118. {
  119. MinWidth = 500,
  120. MinHeight = 436,
  121. Title = msg.Title
  122. };
  123. _window.OnClose += OnWindowClose;
  124. }
  125. private void OnWindowClose()
  126. {
  127. if (_table != null)
  128. {
  129. RaiseNetworkEvent(new TabletopStopPlayingEvent(GetNetEntity(_table.Value)));
  130. }
  131. StopDragging();
  132. _window = null;
  133. }
  134. private bool OnUse(in PointerInputCmdArgs args)
  135. {
  136. if (!_gameTiming.IsFirstTimePredicted)
  137. return false;
  138. return args.State switch
  139. {
  140. BoundKeyState.Down => OnMouseDown(args),
  141. BoundKeyState.Up => OnMouseUp(args),
  142. _ => false
  143. };
  144. }
  145. private bool OnUseSecondary(in PointerInputCmdArgs args)
  146. {
  147. if (_draggedEntity != null && _table != null)
  148. {
  149. var ev = new TabletopRequestTakeOut
  150. {
  151. Entity = GetNetEntity(_draggedEntity.Value),
  152. TableUid = GetNetEntity(_table.Value)
  153. };
  154. RaiseNetworkEvent(ev);
  155. }
  156. return false;
  157. }
  158. private bool OnMouseDown(in PointerInputCmdArgs args)
  159. {
  160. // Return if no player entity
  161. if (_playerManager.LocalEntity is not { } playerEntity)
  162. return false;
  163. var entity = args.EntityUid;
  164. // Return if can not see table or stunned/no hands
  165. if (!CanSeeTable(playerEntity, _table) || !CanDrag(playerEntity, entity, out _))
  166. {
  167. return false;
  168. }
  169. // Try to get the viewport under the cursor
  170. if (_uiManger.MouseGetControl(args.ScreenCoordinates) as ScalingViewport is not { } viewport)
  171. {
  172. return false;
  173. }
  174. StartDragging(entity, viewport);
  175. return true;
  176. }
  177. private bool OnMouseUp(in PointerInputCmdArgs args)
  178. {
  179. StopDragging();
  180. return false;
  181. }
  182. private void OnAppearanceChange(EntityUid uid, TabletopDraggableComponent comp, ref AppearanceChangeEvent args)
  183. {
  184. if (args.Sprite == null)
  185. return;
  186. // TODO: maybe this can work more nicely, by maybe only having to set the item to "being dragged", and have
  187. // the appearance handle the rest
  188. if (_appearance.TryGetData<Vector2>(uid, TabletopItemVisuals.Scale, out var scale, args.Component))
  189. {
  190. args.Sprite.Scale = scale;
  191. }
  192. if (_appearance.TryGetData<int>(uid, TabletopItemVisuals.DrawDepth, out var drawDepth, args.Component))
  193. {
  194. args.Sprite.DrawDepth = drawDepth;
  195. }
  196. }
  197. #endregion
  198. #region Utility
  199. /// <summary>
  200. /// Start dragging an entity in a specific viewport.
  201. /// </summary>
  202. /// <param name="draggedEntity">The entity that we start dragging.</param>
  203. /// <param name="viewport">The viewport in which we are dragging.</param>
  204. private void StartDragging(EntityUid draggedEntity, ScalingViewport viewport)
  205. {
  206. RaisePredictiveEvent(new TabletopDraggingPlayerChangedEvent(GetNetEntity(draggedEntity), true));
  207. _draggedEntity = draggedEntity;
  208. _viewport = viewport;
  209. }
  210. /// <summary>
  211. /// Stop dragging the entity.
  212. /// </summary>
  213. /// <param name="broadcast">Whether to tell other clients that we stopped dragging.</param>
  214. private void StopDragging(bool broadcast = true)
  215. {
  216. // Set the dragging player on the component to noone
  217. if (broadcast && _draggedEntity != null && EntityManager.HasComponent<TabletopDraggableComponent>(_draggedEntity.Value))
  218. {
  219. RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), Transforms.GetMapCoordinates(_draggedEntity.Value), GetNetEntity(_table!.Value)));
  220. RaisePredictiveEvent(new TabletopDraggingPlayerChangedEvent(GetNetEntity(_draggedEntity.Value), false));
  221. }
  222. _draggedEntity = null;
  223. _viewport = null;
  224. }
  225. /// <summary>
  226. /// Clamps coordinates within a viewport. ONLY WORKS FOR 90 DEGREE ROTATIONS!
  227. /// </summary>
  228. /// <param name="coordinates">The coordinates to be clamped.</param>
  229. /// <param name="viewport">The viewport to clamp the coordinates to.</param>
  230. /// <returns>Coordinates clamped to the viewport.</returns>
  231. private static MapCoordinates ClampPositionToViewport(MapCoordinates coordinates, ScalingViewport viewport)
  232. {
  233. if (coordinates == MapCoordinates.Nullspace) return MapCoordinates.Nullspace;
  234. var eye = viewport.Eye;
  235. if (eye == null)
  236. return MapCoordinates.Nullspace;
  237. var size = (Vector2) viewport.ViewportSize / EyeManager.PixelsPerMeter; // Convert to tiles instead of pixels
  238. var eyePosition = eye.Position.Position;
  239. var eyeRotation = eye.Rotation;
  240. var eyeScale = eye.Scale;
  241. var min = (eyePosition - size / 2) / eyeScale;
  242. var max = (eyePosition + size / 2) / eyeScale;
  243. // If 90/270 degrees rotated, flip X and Y
  244. if (MathHelper.CloseToPercent(eyeRotation.Degrees % 180d, 90d) || MathHelper.CloseToPercent(eyeRotation.Degrees % 180d, -90d))
  245. {
  246. (min.Y, min.X) = (min.X, min.Y);
  247. (max.Y, max.X) = (max.X, max.Y);
  248. }
  249. var clampedPosition = Vector2.Clamp(coordinates.Position, min, max);
  250. // Use the eye's map ID, we don't want anything moving to a different map!
  251. return new MapCoordinates(clampedPosition, eye.Position.MapId);
  252. }
  253. #endregion
  254. }
  255. }