diff --git a/Content.Server/Tabletop/Components/TabletopGameComponent.cs b/Content.Server/Tabletop/Components/TabletopGameComponent.cs index 6e3a5b9105..88f5e6ea46 100644 --- a/Content.Server/Tabletop/Components/TabletopGameComponent.cs +++ b/Content.Server/Tabletop/Components/TabletopGameComponent.cs @@ -1,16 +1,19 @@ using Content.Shared.ActionBlocker; using Content.Shared.Verbs; +using Robust.Server.GameObjects; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.Localization; using Robust.Shared.Maths; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.ViewVariables; namespace Content.Server.Tabletop.Components { /// /// A component that makes an object playable as a tabletop game. /// - [RegisterComponent] + [RegisterComponent, Friend(typeof(TabletopSystem))] public class TabletopGameComponent : Component { public override string Name => "TabletopGame"; @@ -27,6 +30,9 @@ namespace Content.Server.Tabletop.Components [DataField("cameraZoom")] public Vector2 CameraZoom { get; } = Vector2.One; + [ViewVariables] + public TabletopSession? Session { get; set; } = null; + /// /// A verb that allows the player to start playing a tabletop game. /// @@ -35,7 +41,7 @@ namespace Content.Server.Tabletop.Components { protected override void GetData(IEntity user, TabletopGameComponent component, VerbData data) { - if (!EntitySystem.Get().CanInteract(user)) + if (!user.HasComponent() || !EntitySystem.Get().CanInteract(user)) { data.Visibility = VerbVisibility.Invisible; return; @@ -47,7 +53,8 @@ namespace Content.Server.Tabletop.Components protected override void Activate(IEntity user, TabletopGameComponent component) { - EntitySystem.Get().OpenTable(user, component.Owner); + if(user.TryGetComponent(out ActorComponent? actor)) + EntitySystem.Get().OpenSessionFor(actor.PlayerSession, component.Owner.Uid); } } } diff --git a/Content.Server/Tabletop/Components/TabletopGamerComponent.cs b/Content.Server/Tabletop/Components/TabletopGamerComponent.cs new file mode 100644 index 0000000000..6eb95f3afc --- /dev/null +++ b/Content.Server/Tabletop/Components/TabletopGamerComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Tabletop.Components +{ + /// + /// Component for marking an entity as currently playing a tabletop. + /// + [RegisterComponent, Friend(typeof(TabletopSystem))] + public class TabletopGamerComponent : Component + { + public override string Name => "TabletopGamer"; + + [DataField("tabletop")] + public EntityUid Tabletop { get; set; } = EntityUid.Invalid; + } +} diff --git a/Content.Server/Tabletop/TabletopChessSetup.cs b/Content.Server/Tabletop/TabletopChessSetup.cs index 9b5dfbd09d..f7f08d092e 100644 --- a/Content.Server/Tabletop/TabletopChessSetup.cs +++ b/Content.Server/Tabletop/TabletopChessSetup.cs @@ -1,9 +1,11 @@ +using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Tabletop { + [UsedImplicitly] public class TabletopChessSetup : TabletopSetup { [DataField("boardPrototype")] @@ -11,33 +13,34 @@ namespace Content.Server.Tabletop // TODO: Un-hardcode the rest of entity prototype IDs, probably. - public override void SetupTabletop(MapId mapId, IEntityManager entityManager) + public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) { - var chessboard = entityManager.SpawnEntity(ChessBoardPrototype, new MapCoordinates(-1, 0, mapId)); - chessboard.Transform.Anchored = true; + var chessboard = entityManager.SpawnEntity(ChessBoardPrototype, session.Position.Offset(-1, 0)); - SpawnPieces(entityManager, new MapCoordinates(-4.5f, 3.5f, mapId)); + session.Entities.Add(chessboard.Uid); + + SpawnPieces(session, entityManager, session.Position.Offset(-4.5f, 3.5f)); } - private void SpawnPieces(IEntityManager entityManager, MapCoordinates topLeft, float separation = 1f) + private void SpawnPieces(TabletopSession session, IEntityManager entityManager, MapCoordinates topLeft, float separation = 1f) { var (mapId, x, y) = topLeft; // Spawn all black pieces - SpawnPiecesRow(entityManager, "Black", topLeft, separation); - SpawnPawns(entityManager, "Black", new MapCoordinates(x, y - separation, mapId) , separation); + SpawnPiecesRow(session, entityManager, "Black", topLeft, separation); + SpawnPawns(session, entityManager, "Black", new MapCoordinates(x, y - separation, mapId) , separation); // Spawn all white pieces - SpawnPawns(entityManager, "White", new MapCoordinates(x, y - 6 * separation, mapId) , separation); - SpawnPiecesRow(entityManager, "White", new MapCoordinates(x, y - 7 * separation, mapId), separation); + SpawnPawns(session, entityManager, "White", new MapCoordinates(x, y - 6 * separation, mapId) , separation); + SpawnPiecesRow(session, entityManager, "White", new MapCoordinates(x, y - 7 * separation, mapId), separation); // Extra queens - entityManager.SpawnEntity("BlackQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 3 * separation, mapId)); - entityManager.SpawnEntity("WhiteQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 4 * separation, mapId)); + session.Entities.Add(entityManager.SpawnEntity("BlackQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 3 * separation, mapId)).Uid); + session.Entities.Add(entityManager.SpawnEntity("WhiteQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 4 * separation, mapId)).Uid); } // TODO: refactor to load FEN instead - private void SpawnPiecesRow(IEntityManager entityManager, string color, MapCoordinates left, float separation = 1f) + private void SpawnPiecesRow(TabletopSession session, IEntityManager entityManager, string color, MapCoordinates left, float separation = 1f) { const string piecesRow = "rnbqkbnr"; @@ -48,32 +51,32 @@ namespace Content.Server.Tabletop switch (piecesRow[i]) { case 'r': - entityManager.SpawnEntity(color + "Rook", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(entityManager.SpawnEntity(color + "Rook", new MapCoordinates(x + i * separation, y, mapId)).Uid); break; case 'n': - entityManager.SpawnEntity(color + "Knight", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(entityManager.SpawnEntity(color + "Knight", new MapCoordinates(x + i * separation, y, mapId)).Uid); break; case 'b': - entityManager.SpawnEntity(color + "Bishop", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(entityManager.SpawnEntity(color + "Bishop", new MapCoordinates(x + i * separation, y, mapId)).Uid); break; case 'q': - entityManager.SpawnEntity(color + "Queen", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(entityManager.SpawnEntity(color + "Queen", new MapCoordinates(x + i * separation, y, mapId)).Uid); break; case 'k': - entityManager.SpawnEntity(color + "King", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(entityManager.SpawnEntity(color + "King", new MapCoordinates(x + i * separation, y, mapId)).Uid); break; } } } // TODO: refactor to load FEN instead - private void SpawnPawns(IEntityManager entityManager, string color, MapCoordinates left, float separation = 1f) + private void SpawnPawns(TabletopSession session, IEntityManager entityManager, string color, MapCoordinates left, float separation = 1f) { var (mapId, x, y) = left; for (int i = 0; i < 8; i++) { - entityManager.SpawnEntity(color + "Pawn", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(entityManager.SpawnEntity(color + "Pawn", new MapCoordinates(x + i * separation, y, mapId)).Uid); } } } diff --git a/Content.Server/Tabletop/TabletopParchisSetup.cs b/Content.Server/Tabletop/TabletopParchisSetup.cs index 89d1001851..c6de3f916d 100644 --- a/Content.Server/Tabletop/TabletopParchisSetup.cs +++ b/Content.Server/Tabletop/TabletopParchisSetup.cs @@ -1,9 +1,10 @@ +using JetBrains.Annotations; using Robust.Shared.GameObjects; -using Robust.Shared.Map; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Tabletop { + [UsedImplicitly] public class TabletopParchisSetup : TabletopSetup { [DataField("boardPrototype")] @@ -21,10 +22,9 @@ namespace Content.Server.Tabletop [DataField("bluePiecePrototype")] public string BluePiecePrototype { get; } = "BlueParchisPiece"; - public override void SetupTabletop(MapId mapId, IEntityManager entityManager) + public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) { - var board = entityManager.SpawnEntity(ParchisBoardPrototype, new MapCoordinates(0, 0, mapId)); - board.Transform.Anchored = true; + var board = entityManager.SpawnEntity(ParchisBoardPrototype, session.Position); const float x1 = 6.25f; const float x2 = 4.25f; @@ -32,29 +32,31 @@ namespace Content.Server.Tabletop const float y1 = 6.25f; const float y2 = 4.25f; + var center = session.Position; + // Red pieces. - entityManager.SpawnEntity(RedPiecePrototype, new MapCoordinates(-x1, -y1, mapId)); - entityManager.SpawnEntity(RedPiecePrototype, new MapCoordinates(-x1, -y2, mapId)); - entityManager.SpawnEntity(RedPiecePrototype, new MapCoordinates(-x2, -y1, mapId)); - entityManager.SpawnEntity(RedPiecePrototype, new MapCoordinates(-x2, -y2, mapId)); + session.Entities.Add(entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y1)).Uid); + session.Entities.Add(entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y2)).Uid); + session.Entities.Add(entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y1)).Uid); + session.Entities.Add(entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y2)).Uid); // Green pieces. - entityManager.SpawnEntity(GreenPiecePrototype, new MapCoordinates(x1, -y1, mapId)); - entityManager.SpawnEntity(GreenPiecePrototype, new MapCoordinates(x1, -y2, mapId)); - entityManager.SpawnEntity(GreenPiecePrototype, new MapCoordinates(x2, -y1, mapId)); - entityManager.SpawnEntity(GreenPiecePrototype, new MapCoordinates(x2, -y2, mapId)); + session.Entities.Add(entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y1)).Uid); + session.Entities.Add(entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y2)).Uid); + session.Entities.Add(entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y1)).Uid); + session.Entities.Add(entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y2)).Uid); // Yellow pieces. - entityManager.SpawnEntity(YellowPiecePrototype, new MapCoordinates(x1, y1, mapId)); - entityManager.SpawnEntity(YellowPiecePrototype, new MapCoordinates(x1, y2, mapId)); - entityManager.SpawnEntity(YellowPiecePrototype, new MapCoordinates(x2, y1, mapId)); - entityManager.SpawnEntity(YellowPiecePrototype, new MapCoordinates(x2, y2, mapId)); + session.Entities.Add(entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y1)).Uid); + session.Entities.Add(entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y2)).Uid); + session.Entities.Add(entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y1)).Uid); + session.Entities.Add(entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y2)).Uid); // Blue pieces. - entityManager.SpawnEntity(BluePiecePrototype, new MapCoordinates(-x1, y1, mapId)); - entityManager.SpawnEntity(BluePiecePrototype, new MapCoordinates(-x1, y2, mapId)); - entityManager.SpawnEntity(BluePiecePrototype, new MapCoordinates(-x2, y1, mapId)); - entityManager.SpawnEntity(BluePiecePrototype, new MapCoordinates(-x2, y2, mapId)); + session.Entities.Add(entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y1)).Uid); + session.Entities.Add(entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y2)).Uid); + session.Entities.Add(entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y1)).Uid); + session.Entities.Add(entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y2)).Uid); } } } diff --git a/Content.Server/Tabletop/TabletopSession.cs b/Content.Server/Tabletop/TabletopSession.cs index 5f16ada710..8aa1bbfe8a 100644 --- a/Content.Server/Tabletop/TabletopSession.cs +++ b/Content.Server/Tabletop/TabletopSession.cs @@ -1,55 +1,34 @@ using System.Collections.Generic; using Robust.Server.Player; +using Robust.Shared.GameObjects; using Robust.Shared.Map; +using Robust.Shared.Maths; namespace Content.Server.Tabletop { /// - /// A struct for storing data about a running tabletop game. + /// A class for storing data about a running tabletop game. /// - public struct TabletopSession + public class TabletopSession { /// - /// The map ID associated with this tabletop game session. + /// The center position of this session. /// - public MapId MapId; + public readonly MapCoordinates Position; /// - /// The set of players currently playing this tabletop game. + /// The set of players currently playing this tabletop game. /// - private readonly HashSet _currentPlayers; - - /// The map ID associated with this tabletop game. - public TabletopSession(MapId mapId) - { - MapId = mapId; - _currentPlayers = new(); - } + public readonly Dictionary Players = new(); /// - /// Returns true if the given player is currently playing this tabletop game. + /// All entities bound to this session. If you create an entity for this session, you have to add it here. /// - public bool IsPlaying(IPlayerSession playerSession) - { - return _currentPlayers.Contains(playerSession); - } + public readonly HashSet Entities = new(); - /// - /// Store that this player has started playing this tabletop game. If the player was already playing, nothing - /// happens. - /// - public void StartPlaying(IPlayerSession playerSession) + public TabletopSession(MapId tabletopMap, Vector2 position) { - _currentPlayers.Add(playerSession); - } - - /// - /// Store that this player has stopped playing this tabletop game. If the player was not playing, nothing - /// happens. - /// - public void StopPlaying(IPlayerSession playerSession) - { - _currentPlayers.Remove(playerSession); + Position = new MapCoordinates(position, tabletopMap); } } } diff --git a/Content.Server/Tabletop/TabletopSessionPlayerData.cs b/Content.Server/Tabletop/TabletopSessionPlayerData.cs new file mode 100644 index 0000000000..556b53d16c --- /dev/null +++ b/Content.Server/Tabletop/TabletopSessionPlayerData.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.Tabletop +{ + /// + /// A class that stores per-player data for tabletops. + /// + public class TabletopSessionPlayerData + { + public EntityUid Camera { get; set; } + } +} diff --git a/Content.Server/Tabletop/TabletopSetup.cs b/Content.Server/Tabletop/TabletopSetup.cs index 75710dc92f..6f0ecae0ab 100644 --- a/Content.Server/Tabletop/TabletopSetup.cs +++ b/Content.Server/Tabletop/TabletopSetup.cs @@ -1,5 +1,4 @@ using Robust.Shared.GameObjects; -using Robust.Shared.Map; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Tabletop @@ -7,6 +6,12 @@ namespace Content.Server.Tabletop [ImplicitDataDefinitionForInheritors] public abstract class TabletopSetup { - public abstract void SetupTabletop(MapId mapId, IEntityManager entityManager); + /// + /// Method for setting up a tabletop. Use this to spawn the board and pieces, etc. + /// Make sure you add every entity you create to the Entities hashset in the session. + /// + /// Tabletop session to set up. You'll want to grab the tabletop center position here for spawning entities. + /// Dependency that can be used for spawning entities. + public abstract void SetupTabletop(TabletopSession session, IEntityManager entityManager); } } diff --git a/Content.Server/Tabletop/TabletopSystem.Draggable.cs b/Content.Server/Tabletop/TabletopSystem.Draggable.cs new file mode 100644 index 0000000000..7e31ba7909 --- /dev/null +++ b/Content.Server/Tabletop/TabletopSystem.Draggable.cs @@ -0,0 +1,88 @@ +using Content.Server.Tabletop.Components; +using Content.Shared.Tabletop; +using Content.Shared.Tabletop.Events; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using DrawDepth = Content.Shared.DrawDepth.DrawDepth; + +namespace Content.Server.Tabletop +{ + public partial class TabletopSystem + { + public void InitializeDraggable() + { + SubscribeNetworkEvent(OnTabletopMove); + SubscribeNetworkEvent(OnDraggingPlayerChanged); + SubscribeLocalEvent(GetDraggableState); + } + + /// + /// Move an entity which is dragged by the user, but check if they are allowed to do so and to these coordinates + /// + private void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventArgs args) + { + if (args.SenderSession as IPlayerSession is not { AttachedEntity: { } playerEntity } playerSession) + return; + + if (!ComponentManager.TryGetComponent(msg.TableUid, out TabletopGameComponent? tabletop) || tabletop.Session is not {} session) + return; + + // Check if player is actually playing at this table + if (!session.Players.ContainsKey(playerSession)) + return; + + // Return if can not see table or stunned/no hands + if (!EntityManager.TryGetEntity(msg.TableUid, out var table)) + return; + + if (!CanSeeTable(playerEntity, table) || StunnedOrNoHands(playerEntity)) + return; + + // Check if moved entity exists and has tabletop draggable component + if (!EntityManager.TryGetEntity(msg.MovedEntityUid, out var movedEntity)) + return; + + if (!ComponentManager.HasComponent(movedEntity.Uid)) + return; + + // TODO: some permission system, disallow movement if you're not permitted to move the item + + // Move the entity and dirty it (we use the map ID from the entity so noone can try to be funny and move the item to another map) + var transform = ComponentManager.GetComponent(movedEntity.Uid); + var entityCoordinates = new EntityCoordinates(_mapManager.GetMapEntityId(transform.MapID), msg.Coordinates.Position); + transform.Coordinates = entityCoordinates; + movedEntity.Dirty(); + } + + private void OnDraggingPlayerChanged(TabletopDraggingPlayerChangedEvent msg) + { + var draggedEntity = EntityManager.GetEntity(msg.DraggedEntityUid); + + if (!draggedEntity.TryGetComponent(out var draggableComponent)) return; + + draggableComponent.DraggingPlayer = msg.DraggingPlayer; + + if (!draggedEntity.TryGetComponent(out var appearance)) return; + + if (draggableComponent.DraggingPlayer != null) + { + appearance.SetData(TabletopItemVisuals.Scale, new Vector2(1.25f, 1.25f)); + appearance.SetData(TabletopItemVisuals.DrawDepth, (int) DrawDepth.Items + 1); + } + else + { + appearance.SetData(TabletopItemVisuals.Scale, Vector2.One); + appearance.SetData(TabletopItemVisuals.DrawDepth, (int) DrawDepth.Items); + } + } + + private void GetDraggableState(EntityUid uid, TabletopDraggableComponent component, ref ComponentGetState args) + { + args.State = new TabletopDraggableComponentState(component.DraggingPlayer); + } + } +} diff --git a/Content.Server/Tabletop/TabletopSystem.Map.cs b/Content.Server/Tabletop/TabletopSystem.Map.cs new file mode 100644 index 0000000000..eebd4894d7 --- /dev/null +++ b/Content.Server/Tabletop/TabletopSystem.Map.cs @@ -0,0 +1,102 @@ +using System; +using Content.Shared.GameTicking; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; + +namespace Content.Server.Tabletop +{ + public partial class TabletopSystem + { + /// + /// Separation between tabletops in the tabletop map. + /// + private const int TabletopSeparation = 100; + + /// + /// Map where all tabletops reside. + /// + public MapId TabletopMap { get; private set; } = MapId.Nullspace; + + /// + /// The number of tabletops created in the map. + /// Used for calculating the position of the next one. + /// + private int _tabletops = 0; + + /// + /// Despite the name, this method is only used to subscribe to events. + /// + private void InitializeMap() + { + SubscribeLocalEvent(OnRoundRestart); + } + + /// + /// Gets the next available position for a tabletop, and increments the tabletop count. + /// + /// + private Vector2 GetNextTabletopPosition() + { + return UlamSpiral(_tabletops++) * TabletopSeparation; + } + + /// + /// Ensures that the tabletop map exists. Creates it if it doesn't. + /// + private void EnsureTabletopMap() + { + if (TabletopMap != MapId.Nullspace && _mapManager.MapExists(TabletopMap)) + return; + + TabletopMap = _mapManager.CreateMap(); + _tabletops = 0; + + var mapComp = ComponentManager.GetComponent(_mapManager.GetMapEntityId(TabletopMap)); + + // Lighting is always disabled in tabletop world. + mapComp.LightingEnabled = false; + mapComp.Dirty(); + } + + /// + /// Algorithm for mapping scalars to 2D positions in the same pattern as an Ulam Spiral. + /// + /// Scalar to map to a 2D position. + /// The mapped 2D position for the scalar. + private Vector2i UlamSpiral(int n) + { + var k = (int)MathF.Ceiling(MathF.Sqrt(n) - 1) / 2; + var t = 2 * k + 1; + var m = (int)MathF.Pow(t, 2); + t--; + + if (n >= m - t) + return new Vector2i(k - (m - n), -k); + + m -= t; + + if (n >= m - t) + return new Vector2i(-k, -k + (m - n)); + + m -= t; + + if (n >= m - t) + return new Vector2i(-k + (m - n), k); + + return new Vector2i(k, k - (m - n - t)); + } + + private void OnRoundRestart(RoundRestartCleanupEvent _) + { + if (TabletopMap == MapId.Nullspace || !_mapManager.MapExists(TabletopMap)) + return; + + // This will usually *not* be the case, but better make sure. + _mapManager.DeleteMap(TabletopMap); + + // Reset tabletop count. + _tabletops = 0; + } + } +} diff --git a/Content.Server/Tabletop/TabletopSystem.Session.cs b/Content.Server/Tabletop/TabletopSystem.Session.cs new file mode 100644 index 0000000000..97d3e6cef3 --- /dev/null +++ b/Content.Server/Tabletop/TabletopSystem.Session.cs @@ -0,0 +1,156 @@ +using Content.Server.Tabletop.Components; +using Content.Shared.Tabletop.Events; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using Robust.Shared.Utility; + +namespace Content.Server.Tabletop +{ + public partial class TabletopSystem + { + /// + /// Ensures that a exists on a . + /// Creates it and sets it up if it doesn't. + /// + /// The tabletop game in question. + /// The session for the given tabletop game. + private TabletopSession EnsureSession(TabletopGameComponent tabletop) + { + // We already have a session, return it + // TODO: if tables are connected, treat them as a single entity. This can be done by sharing the session. + if (tabletop.Session != null) + return tabletop.Session; + + // We make sure that the tabletop map exists before continuing. + EnsureTabletopMap(); + + // Create new session. + var session = new TabletopSession(TabletopMap, GetNextTabletopPosition()); + tabletop.Session = session; + + // Since this is the first time opening this session, set up the game + tabletop.Setup.SetupTabletop(session, EntityManager); + + Logger.Info($"Created tabletop session number {tabletop} at position {session.Position}."); + + return session; + } + + /// + /// Cleans up a tabletop game session, deleting every entity in it. + /// + /// The UID of the tabletop game entity. + public void CleanupSession(EntityUid uid) + { + if (!ComponentManager.TryGetComponent(uid, out TabletopGameComponent? tabletop)) + return; + + if (tabletop.Session is not { } session) + return; + + foreach (var (player, _) in session.Players) + { + CloseSessionFor(player, uid); + } + + foreach (var euid in session.Entities) + { + EntityManager.QueueDeleteEntity(euid); + } + + tabletop.Session = null; + } + + /// + /// Adds a player to a tabletop game session, sending a message so the tabletop window opens on their end. + /// + /// The player session in question. + /// The UID of the tabletop game entity. + public void OpenSessionFor(IPlayerSession player, EntityUid uid) + { + if (!ComponentManager.TryGetComponent(uid, out TabletopGameComponent? tabletop) || player.AttachedEntity is not {} attachedEntity) + return; + + // Make sure we have a session, and add the player to it if not added already. + var session = EnsureSession(tabletop); + + if (session.Players.ContainsKey(player)) + return; + + if(attachedEntity.TryGetComponent(out var gamer)) + CloseSessionFor(player, gamer.Tabletop, false); + + // Set the entity as an absolute GAMER. + attachedEntity.EnsureComponent().Tabletop = uid; + + // Create a camera for the gamer to use + var camera = CreateCamera(tabletop, player); + + session.Players[player] = new TabletopSessionPlayerData { Camera = camera }; + + // Tell the gamer to open a viewport for the tabletop game + RaiseNetworkEvent(new TabletopPlayEvent(uid, camera, Loc.GetString(tabletop.BoardName), tabletop.Size), player.ConnectedClient); + } + + /// + /// Removes a player from a tabletop game session, and sends them a message so their tabletop window is closed. + /// + /// The player in question. + /// The UID of the tabletop game entity. + /// Whether to remove the from the player's attached entity. + public void CloseSessionFor(IPlayerSession player, EntityUid uid, bool removeGamerComponent = true) + { + if (!ComponentManager.TryGetComponent(uid, out TabletopGameComponent? tabletop) || tabletop.Session is not { } session) + return; + + if (!session.Players.TryGetValue(player, out var data)) + return; + + if(removeGamerComponent && player.AttachedEntity is {} attachedEntity && attachedEntity.TryGetComponent(out TabletopGamerComponent? gamer)) + { + // We invalidate this to prevent an infinite feedback from removing the component. + gamer.Tabletop = EntityUid.Invalid; + + // You stop being a gamer....... + attachedEntity.RemoveComponent(); + } + + session.Players.Remove(player); + session.Entities.Remove(data.Camera); + + // Deleting the view subscriber automatically cleans up subscriptions, no need to do anything else. + EntityManager.QueueDeleteEntity(data.Camera); + } + + /// + /// A helper method that creates a camera for a specified player, in a tabletop game session. + /// + /// The tabletop game component in question. + /// The player in question. + /// An offset from the tabletop position for the camera. Zero by default. + /// The UID of the camera entity. + private EntityUid CreateCamera(TabletopGameComponent tabletop, IPlayerSession player, Vector2 offset = default) + { + DebugTools.AssertNotNull(tabletop.Session); + + var session = tabletop.Session!; + + // Spawn an empty entity at the coordinates + var camera = EntityManager.SpawnEntity(null, session.Position.Offset(offset)); + + // Add an eye component and disable FOV + var eyeComponent = camera.EnsureComponent(); + eyeComponent.DrawFov = false; + eyeComponent.Zoom = tabletop.CameraZoom; + + // Add the user to the view subscribers. If there is no player session, just skip this step + _viewSubscriberSystem.AddViewSubscriber(camera.Uid, player); + + return camera.Uid; + } + } +} diff --git a/Content.Server/Tabletop/TabletopSystem.cs b/Content.Server/Tabletop/TabletopSystem.cs index 35551592a8..7fe9fd53e5 100644 --- a/Content.Server/Tabletop/TabletopSystem.cs +++ b/Content.Server/Tabletop/TabletopSystem.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using Content.Server.Tabletop.Components; +using Content.Server.Tabletop.Components; using Content.Shared.ActionBlocker; using Content.Shared.Interaction; using Content.Shared.Tabletop; @@ -7,13 +6,10 @@ using Content.Shared.Tabletop.Events; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Server.Player; +using Robust.Shared.Enums; using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Map; -using Robust.Shared.Maths; -using DrawDepth = Content.Shared.DrawDepth.DrawDepth; namespace Content.Server.Tabletop { @@ -24,184 +20,76 @@ namespace Content.Server.Tabletop [Dependency] private readonly ViewSubscriberSystem _viewSubscriberSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; - /// - /// All tabletop games currently in progress. Sessions are associated with an entity UID, which acts as a - /// key, such that an entity can only have one running tabletop game session. - /// - private readonly Dictionary _gameSessions = new(); - public override void Initialize() { - SubscribeNetworkEvent(OnTabletopMove); - SubscribeNetworkEvent(OnDraggingPlayerChanged); SubscribeNetworkEvent(OnStopPlaying); SubscribeLocalEvent(OnTabletopActivate); SubscribeLocalEvent(OnGameShutdown); - SubscribeLocalEvent(GetCompState); + + SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnGamerShutdown); + + InitializeMap(); + InitializeDraggable(); } private void OnTabletopActivate(EntityUid uid, TabletopGameComponent component, ActivateInWorldEvent args) { + // Check that a player is attached to the entity. + if (!ComponentManager.TryGetComponent(args.User.Uid, out ActorComponent? actor)) + return; + + // Check that the entity can interact with the game board. if(_actionBlockerSystem.CanInteract(args.User)) - OpenTable(args.User, args.Target); + OpenSessionFor(actor.PlayerSession, uid); } - private void GetCompState(EntityUid uid, TabletopDraggableComponent component, ref ComponentGetState args) + private void OnGameShutdown(EntityUid uid, TabletopGameComponent component, ComponentShutdown args) { - args.State = new TabletopDraggableComponentState(component.DraggingPlayer); - } - - /// - /// For a specific user, create a table if it does not exist yet and let the user open a UI window to play it. - /// - /// The user entity for which to open the window. - /// The entity with which the tabletop game session will be associated. - public void OpenTable(IEntity user, IEntity table) - { - if (user.PlayerSession() is not { } playerSession - || !table.TryGetComponent(out TabletopGameComponent? tabletop)) return; - - // Make sure we have a session, and add the player to it - var session = EnsureSession(table.Uid, tabletop); - session.StartPlaying(playerSession); - - // Create a camera for the user to use - // TODO: set correct coordinates, depending on the piece the game was started from - IEntity camera = CreateCamera(tabletop, user, new MapCoordinates(0, 0, session.MapId)); - - // Tell the client to open a viewport for the tabletop game - RaiseNetworkEvent(new TabletopPlayEvent(table.Uid, camera.Uid, Loc.GetString(tabletop.BoardName), tabletop.Size), playerSession.ConnectedClient); - } - - /// - /// Create a session associated to this entity UID, if it does not already exist, and return it. - /// - /// The entity UID to ensure a session for. - /// The created/stored tabletop game session. - private TabletopSession EnsureSession(EntityUid uid, TabletopGameComponent tabletop) - { - // We already have a session, return it - // TODO: if tables are connected, treat them as a single entity - if (_gameSessions.ContainsKey(uid)) - { - return _gameSessions[uid]; - } - - // Session does not exist for this entity yet, create a map and create a session - var mapId = _mapManager.CreateMap(); - - // Tabletop maps do not need lighting, turn it off - var mapComponent = _mapManager.GetMapEntity(mapId).GetComponent(); - mapComponent.LightingEnabled = false; - mapComponent.Dirty(); - - _gameSessions.Add(uid, new TabletopSession(mapId)); - var session = _gameSessions[uid]; - - // Since this is the first time opening this session, set up the game - tabletop.Setup.SetupTabletop(session.MapId, EntityManager); - - return session; - } - - #region Event handlers - - // Move an entity which is dragged by the user, but check if they are allowed to do so and to these coordinates - private void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventArgs args) - { - if (args.SenderSession as IPlayerSession is not { AttachedEntity: { } playerEntity } playerSession) return; - - // Check if player is actually playing at this table - if (!_gameSessions.TryGetValue(msg.TableUid, out var tableUid) || - !tableUid.IsPlaying(playerSession)) return; - - // Return if can not see table or stunned/no hands - if (!EntityManager.TryGetEntity(msg.TableUid, out var table)) return; - if (!CanSeeTable(playerEntity, table) || StunnedOrNoHands(playerEntity)) return; - - // Check if moved entity exists and has tabletop draggable component - if (!EntityManager.TryGetEntity(msg.MovedEntityUid, out var movedEntity)) return; - if (!ComponentManager.HasComponent(movedEntity.Uid)) return; - - // TODO: some permission system, disallow movement if you're not permitted to move the item - - // Move the entity and dirty it (we use the map ID from the entity so noone can try to be funny and move the item to another map) - var transform = ComponentManager.GetComponent(movedEntity.Uid); - var entityCoordinates = new EntityCoordinates(_mapManager.GetMapEntityId(transform.MapID), msg.Coordinates.Position); - transform.Coordinates = entityCoordinates; - movedEntity.Dirty(); - } - - private void OnDraggingPlayerChanged(TabletopDraggingPlayerChangedEvent msg) - { - var draggedEntity = EntityManager.GetEntity(msg.DraggedEntityUid); - - if (!draggedEntity.TryGetComponent(out var draggableComponent)) return; - - draggableComponent.DraggingPlayer = msg.DraggingPlayer; - - if (!draggedEntity.TryGetComponent(out var appearance)) return; - - if (draggableComponent.DraggingPlayer != null) - { - appearance.SetData(TabletopItemVisuals.Scale, new Vector2(1.25f, 1.25f)); - appearance.SetData(TabletopItemVisuals.DrawDepth, (int) DrawDepth.Items + 1); - } - else - { - appearance.SetData(TabletopItemVisuals.Scale, Vector2.One); - appearance.SetData(TabletopItemVisuals.DrawDepth, (int) DrawDepth.Items); - } + CleanupSession(uid); } private void OnStopPlaying(TabletopStopPlayingEvent msg, EntitySessionEventArgs args) { - if (_gameSessions.ContainsKey(msg.TableUid) && args.SenderSession as IPlayerSession is { } playerSession) + CloseSessionFor((IPlayerSession)args.SenderSession, msg.TableUid); + } + + private void OnPlayerDetached(EntityUid uid, TabletopGamerComponent component, PlayerDetachedEvent args) + { + if(component.Tabletop.IsValid()) + CloseSessionFor(args.Player, component.Tabletop); + } + + private void OnGamerShutdown(EntityUid uid, TabletopGamerComponent component, ComponentShutdown args) + { + if (!ComponentManager.TryGetComponent(uid, out ActorComponent? actor)) + return; + + if(component.Tabletop.IsValid()) + CloseSessionFor(actor.PlayerSession, component.Tabletop); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + foreach (var gamer in ComponentManager.EntityQuery(true)) { - _gameSessions[msg.TableUid].StopPlaying(playerSession); + if (!EntityManager.TryGetEntity(gamer.Tabletop, out var table)) + continue; + + if (!gamer.Owner.TryGetComponent(out ActorComponent? actor)) + { + gamer.Owner.RemoveComponent(); + return; + }; + + if (actor.PlayerSession.Status > SessionStatus.Connected || CanSeeTable(gamer.Owner, table) + || !StunnedOrNoHands(gamer.Owner)) + continue; + + CloseSessionFor(actor.PlayerSession, table.Uid); } } - - // TODO: needs to be refactored such that the corresponding entity on the table gets removed, instead of the whole map - private void OnGameShutdown(EntityUid uid, TabletopGameComponent component, ComponentShutdown args) - { - if (!_gameSessions.ContainsKey(uid)) return; - - // Delete the map and remove it from the list of sessions - _mapManager.DeleteMap(_gameSessions[uid].MapId); - _gameSessions.Remove(uid); - } - - #endregion - - #region Utility - - /// - /// Create a camera entity for a user to control, and add the user to the view subscribers. - /// - /// The tabletop to create the camera for. - /// The user entity to create this camera for and add to the view subscribers. - /// The map coordinates to spawn this camera at. - // TODO: this can probably be generalized into a "CctvSystem" or whatever - private IEntity CreateCamera(TabletopGameComponent tabletop, IEntity user, MapCoordinates coordinates) - { - // Spawn an empty entity at the coordinates - var camera = EntityManager.SpawnEntity(null, coordinates); - - // Add an eye component and disable FOV - var eyeComponent = camera.EnsureComponent(); - eyeComponent.DrawFov = false; - eyeComponent.Zoom = tabletop.CameraZoom; - - // Add the user to the view subscribers. If there is no player session, just skip this step - if (user.PlayerSession() is { } playerSession) - { - _viewSubscriberSystem.AddViewSubscriber(camera.Uid, playerSession); - } - - return camera; - } - - #endregion } }