Files
tbd-station-14/Content.Client/Tabletop/TabletopSystem.cs
Pieter-Jan Briers 68ce53ae17 Random spontaneous cleanup PR (#25131)
* Use new Subs.CVar helper

Removes manual config OnValueChanged calls, removes need to remember to manually unsubscribe.

This both reduces boilerplate and fixes many issues where subscriptions weren't removed on entity system shutdown.

* Fix a bunch of warnings

* More warning fixes

* Use new DateTime serializer to get rid of ISerializationHooks in changelog code.

* Get rid of some more ISerializationHooks for enums

* And a little more

* Apply suggestions from code review

Co-authored-by: 0x6273 <0x40@keemail.me>

---------

Co-authored-by: 0x6273 <0x40@keemail.me>
2024-02-13 16:48:39 -05:00

306 lines
12 KiB
C#

using System.Numerics;
using Content.Client.Tabletop.UI;
using Content.Client.Viewport;
using Content.Shared.Tabletop;
using Content.Shared.Tabletop.Components;
using Content.Shared.Tabletop.Events;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Timing;
using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
namespace Content.Client.Tabletop
{
[UsedImplicitly]
public sealed class TabletopSystem : SharedTabletopSystem
{
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IUserInterfaceManager _uiManger = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
// Time in seconds to wait until sending the location of a dragged entity to the server again
private const float Delay = 1f / 10; // 10 Hz
private float _timePassed; // Time passed since last update sent to the server.
private EntityUid? _draggedEntity; // Entity being dragged
private ScalingViewport? _viewport; // Viewport currently being used
private DefaultWindow? _window; // Current open tabletop window (only allow one at a time)
private EntityUid? _table; // The table entity of the currently open game session
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = true;
CommandBinds.Builder
.Bind(EngineKeyFunctions.Use, new PointerInputCmdHandler(OnUse, false, true))
.Bind(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnUseSecondary, true, true))
.Register<TabletopSystem>();
SubscribeNetworkEvent<TabletopPlayEvent>(OnTabletopPlay);
SubscribeLocalEvent<TabletopDraggableComponent, ComponentRemove>(HandleDraggableRemoved);
SubscribeLocalEvent<TabletopDraggableComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void HandleDraggableRemoved(EntityUid uid, TabletopDraggableComponent component, ComponentRemove args)
{
if (_draggedEntity == uid)
StopDragging(false);
}
public override void FrameUpdate(float frameTime)
{
if (_window == null)
return;
// If there is no player entity, return
if (_playerManager.LocalEntity is not { } playerEntity)
return;
if (!CanSeeTable(playerEntity, _table))
{
StopDragging();
_window?.Close();
return;
}
// If no entity is being dragged or no viewport is clicked, return
if (_draggedEntity == null || _viewport == null) return;
if (!CanDrag(playerEntity, _draggedEntity.Value, out var draggableComponent))
{
StopDragging();
return;
}
// If the dragged entity has another dragging player, drop the item
// This should happen if the local player is dragging an item, and another player grabs it out of their hand
if (draggableComponent.DraggingPlayer != null &&
draggableComponent.DraggingPlayer != _playerManager.LocalSession!.UserId)
{
StopDragging(false);
return;
}
// Map mouse position to EntityCoordinates
var coords = _viewport.PixelToMap(_inputManager.MouseScreenPosition.Position);
// Clamp coordinates to viewport
var clampedCoords = ClampPositionToViewport(coords, _viewport);
if (clampedCoords.Equals(MapCoordinates.Nullspace)) return;
// Move the entity locally every update
EntityManager.GetComponent<TransformComponent>(_draggedEntity.Value).WorldPosition = clampedCoords.Position;
// Increment total time passed
_timePassed += frameTime;
// Only send new position to server when Delay is reached
if (_timePassed >= Delay && _table != null)
{
RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), clampedCoords, GetNetEntity(_table.Value)));
_timePassed -= Delay;
}
}
#region Event handlers
/// <summary>
/// Runs when the player presses the "Play Game" verb on a tabletop game.
/// Opens a viewport where they can then play the game.
/// </summary>
private void OnTabletopPlay(TabletopPlayEvent msg)
{
// Close the currently opened window, if it exists
_window?.Close();
_table = GetEntity(msg.TableUid);
// Get the camera entity that the server has created for us
var camera = GetEntity(msg.CameraUid);
if (!EntityManager.TryGetComponent<EyeComponent>(camera, out var eyeComponent))
{
// If there is no eye, print error and do not open any window
Log.Error("Camera entity does not have eye component!");
return;
}
// Create a window to contain the viewport
_window = new TabletopWindow(eyeComponent.Eye, (msg.Size.X, msg.Size.Y))
{
MinWidth = 500,
MinHeight = 436,
Title = msg.Title
};
_window.OnClose += OnWindowClose;
}
private void OnWindowClose()
{
if (_table != null)
{
RaiseNetworkEvent(new TabletopStopPlayingEvent(GetNetEntity(_table.Value)));
}
StopDragging();
_window = null;
}
private bool OnUse(in PointerInputCmdArgs args)
{
if (!_gameTiming.IsFirstTimePredicted)
return false;
return args.State switch
{
BoundKeyState.Down => OnMouseDown(args),
BoundKeyState.Up => OnMouseUp(args),
_ => false
};
}
private bool OnUseSecondary(in PointerInputCmdArgs args)
{
if (_draggedEntity != null && _table != null)
{
var ev = new TabletopRequestTakeOut
{
Entity = GetNetEntity(_draggedEntity.Value),
TableUid = GetNetEntity(_table.Value)
};
RaiseNetworkEvent(ev);
}
return false;
}
private bool OnMouseDown(in PointerInputCmdArgs args)
{
// Return if no player entity
if (_playerManager.LocalEntity is not { } playerEntity)
return false;
var entity = args.EntityUid;
// Return if can not see table or stunned/no hands
if (!CanSeeTable(playerEntity, _table) || !CanDrag(playerEntity, entity, out _))
{
return false;
}
// Try to get the viewport under the cursor
if (_uiManger.MouseGetControl(args.ScreenCoordinates) as ScalingViewport is not { } viewport)
{
return false;
}
StartDragging(entity, viewport);
return true;
}
private bool OnMouseUp(in PointerInputCmdArgs args)
{
StopDragging();
return false;
}
private void OnAppearanceChange(EntityUid uid, TabletopDraggableComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
// TODO: maybe this can work more nicely, by maybe only having to set the item to "being dragged", and have
// the appearance handle the rest
if (_appearance.TryGetData<Vector2>(uid, TabletopItemVisuals.Scale, out var scale, args.Component))
{
args.Sprite.Scale = scale;
}
if (_appearance.TryGetData<int>(uid, TabletopItemVisuals.DrawDepth, out var drawDepth, args.Component))
{
args.Sprite.DrawDepth = drawDepth;
}
}
#endregion
#region Utility
/// <summary>
/// Start dragging an entity in a specific viewport.
/// </summary>
/// <param name="draggedEntity">The entity that we start dragging.</param>
/// <param name="viewport">The viewport in which we are dragging.</param>
private void StartDragging(EntityUid draggedEntity, ScalingViewport viewport)
{
RaisePredictiveEvent(new TabletopDraggingPlayerChangedEvent(GetNetEntity(draggedEntity), true));
_draggedEntity = draggedEntity;
_viewport = viewport;
}
/// <summary>
/// Stop dragging the entity.
/// </summary>
/// <param name="broadcast">Whether to tell other clients that we stopped dragging.</param>
private void StopDragging(bool broadcast = true)
{
// Set the dragging player on the component to noone
if (broadcast && _draggedEntity != null && EntityManager.HasComponent<TabletopDraggableComponent>(_draggedEntity.Value))
{
RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), Transform(_draggedEntity.Value).MapPosition, GetNetEntity(_table!.Value)));
RaisePredictiveEvent(new TabletopDraggingPlayerChangedEvent(GetNetEntity(_draggedEntity.Value), false));
}
_draggedEntity = null;
_viewport = null;
}
/// <summary>
/// Clamps coordinates within a viewport. ONLY WORKS FOR 90 DEGREE ROTATIONS!
/// </summary>
/// <param name="coordinates">The coordinates to be clamped.</param>
/// <param name="viewport">The viewport to clamp the coordinates to.</param>
/// <returns>Coordinates clamped to the viewport.</returns>
private static MapCoordinates ClampPositionToViewport(MapCoordinates coordinates, ScalingViewport viewport)
{
if (coordinates == MapCoordinates.Nullspace) return MapCoordinates.Nullspace;
var eye = viewport.Eye;
if (eye == null) return MapCoordinates.Nullspace;
var size = (Vector2) viewport.ViewportSize / EyeManager.PixelsPerMeter; // Convert to tiles instead of pixels
var eyePosition = eye.Position.Position;
var eyeRotation = eye.Rotation;
var eyeScale = eye.Scale;
var min = (eyePosition - size / 2) / eyeScale;
var max = (eyePosition + size / 2) / eyeScale;
// If 90/270 degrees rotated, flip X and Y
if (MathHelper.CloseToPercent(eyeRotation.Degrees % 180d, 90d) || MathHelper.CloseToPercent(eyeRotation.Degrees % 180d, -90d))
{
(min.Y, min.X) = (min.X, min.Y);
(max.Y, max.X) = (max.X, max.Y);
}
var clampedPosition = Vector2.Clamp(coordinates.Position, min, max);
// Use the eye's map ID, we don't want anything moving to a different map!
return new MapCoordinates(clampedPosition, eye.Position.MapId);
}
#endregion
}
}