From 95a5f7c7dec371272b6ed15bc0decd8ce433865b Mon Sep 17 00:00:00 2001 From: riccardi48 <39341154+riccardi48@users.noreply.github.com> Date: Sat, 27 Jun 2026 12:17:13 +0100 Subject: [PATCH] Table top prediction (#44348) * TabletopSystem-move-to-shared * final refactor * Update TabletopHologramComponent.cs * Update AdminVerbSystem.Smites.cs * merge * line * prediction fix * Fix the scrunkly * Also noticed this --------- Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com> Co-authored-by: metalgearsloth --- Content.Client/Tabletop/TabletopSystem.cs | 19 +- .../Systems/AdminVerbSystem.Smites.cs | 2 - .../Components/TabletopGameComponent.cs | 41 --- .../Components/TabletopGamerComponent.cs | 12 - .../Tabletop/TabletopBackgammonSetup.cs | 67 ---- .../Tabletop/TabletopCheckerSetup.cs | 104 ------ Content.Server/Tabletop/TabletopChessSetup.cs | 89 ----- Content.Server/Tabletop/TabletopEmptySetup.cs | 14 - .../Tabletop/TabletopParchisSetup.cs | 76 ----- Content.Server/Tabletop/TabletopSession.cs | 32 -- .../Tabletop/TabletopSessionPlayerData.cs | 10 - Content.Server/Tabletop/TabletopSetup.cs | 20 -- .../Tabletop/TabletopSystem.Session.cs | 255 +++++++-------- Content.Server/Tabletop/TabletopSystem.cs | 269 +++++----------- .../Components/TabletopGameComponent.cs | 42 +++ .../Components/TabletopGamerComponent.cs | 14 + .../Components}/TabletopHologramComponent.cs | 11 +- .../Tabletop/SharedTabletopSystem.Session.cs | 13 + .../Tabletop/SharedTabletopSystem.cs | 303 ++++++++++++------ .../Tabletop/TabletopBackgammonSetup.cs | 66 ++++ .../Tabletop/TabletopCheckerSetup.cs | 102 ++++++ Content.Shared/Tabletop/TabletopChessSetup.cs | 85 +++++ Content.Shared/Tabletop/TabletopEmptySetup.cs | 13 + .../Tabletop/TabletopParchisSetup.cs | 74 +++++ Content.Shared/Tabletop/TabletopSession.cs | 26 ++ .../Tabletop/TabletopSessionPlayerData.cs | 9 + Content.Shared/Tabletop/TabletopSetup.cs | 19 ++ 27 files changed, 882 insertions(+), 905 deletions(-) delete mode 100644 Content.Server/Tabletop/Components/TabletopGameComponent.cs delete mode 100644 Content.Server/Tabletop/Components/TabletopGamerComponent.cs delete mode 100644 Content.Server/Tabletop/TabletopBackgammonSetup.cs delete mode 100644 Content.Server/Tabletop/TabletopCheckerSetup.cs delete mode 100644 Content.Server/Tabletop/TabletopChessSetup.cs delete mode 100644 Content.Server/Tabletop/TabletopEmptySetup.cs delete mode 100644 Content.Server/Tabletop/TabletopParchisSetup.cs delete mode 100644 Content.Server/Tabletop/TabletopSession.cs delete mode 100644 Content.Server/Tabletop/TabletopSessionPlayerData.cs delete mode 100644 Content.Server/Tabletop/TabletopSetup.cs create mode 100644 Content.Shared/Tabletop/Components/TabletopGameComponent.cs create mode 100644 Content.Shared/Tabletop/Components/TabletopGamerComponent.cs rename {Content.Server/Tabletop => Content.Shared/Tabletop/Components}/TabletopHologramComponent.cs (60%) create mode 100644 Content.Shared/Tabletop/SharedTabletopSystem.Session.cs create mode 100644 Content.Shared/Tabletop/TabletopBackgammonSetup.cs create mode 100644 Content.Shared/Tabletop/TabletopCheckerSetup.cs create mode 100644 Content.Shared/Tabletop/TabletopChessSetup.cs create mode 100644 Content.Shared/Tabletop/TabletopEmptySetup.cs create mode 100644 Content.Shared/Tabletop/TabletopParchisSetup.cs create mode 100644 Content.Shared/Tabletop/TabletopSession.cs create mode 100644 Content.Shared/Tabletop/TabletopSessionPlayerData.cs create mode 100644 Content.Shared/Tabletop/TabletopSetup.cs diff --git a/Content.Client/Tabletop/TabletopSystem.cs b/Content.Client/Tabletop/TabletopSystem.cs index 8b884b31e9..73922ca093 100644 --- a/Content.Client/Tabletop/TabletopSystem.cs +++ b/Content.Client/Tabletop/TabletopSystem.cs @@ -27,13 +27,12 @@ namespace Content.Client.Tabletop [Dependency] private IPlayerManager _playerManager = default!; [Dependency] private IGameTiming _gameTiming = default!; [Dependency] private AppearanceSystem _appearance = default!; - [Dependency] private SharedTransformSystem _transformSystem = default!; + [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private SpriteSystem _sprite = 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 BaseWindow? _window; // Current open tabletop window (only allow one at a time) @@ -60,8 +59,11 @@ namespace Content.Client.Tabletop StopDragging(false); } - public override void FrameUpdate(float frameTime) + public override void Update(float frameTime) { + base.Update(frameTime); + if (!_gameTiming.IsFirstTimePredicted) + return; if (_window == null) return; @@ -101,17 +103,10 @@ namespace Content.Client.Tabletop var clampedCoords = ClampPositionToViewport(coords, _viewport); if (clampedCoords.Equals(MapCoordinates.Nullspace)) return; - // Move the entity locally every update - _transformSystem.SetWorldPosition(_draggedEntity.Value, clampedCoords.Position); - - // Increment total time passed - _timePassed += frameTime; - // Only send new position to server when Delay is reached - if (_timePassed >= Delay && _table != null) + if (_table != null) { RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), clampedCoords, GetNetEntity(_table.Value))); - _timePassed -= Delay; } } @@ -260,7 +255,7 @@ namespace Content.Client.Tabletop // Set the dragging player on the component to noone if (broadcast && _draggedEntity != null && HasComp(_draggedEntity.Value)) { - RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), Transforms.GetMapCoordinates(_draggedEntity.Value), GetNetEntity(_table!.Value))); + RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), _transform.GetMapCoordinates(_draggedEntity.Value), GetNetEntity(_table!.Value))); RaisePredictiveEvent(new TabletopDraggingPlayerChangedEvent(GetNetEntity(_draggedEntity.Value), false)); } diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index de7c4ad308..9acbd95e4a 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -16,11 +16,9 @@ using Content.Server.Speech.Components; using Content.Shared.Speech.Components; using Content.Server.Storage.EntitySystems; using Content.Server.Tabletop; -using Content.Server.Tabletop.Components; using Content.Shared.Actions; using Content.Shared.Administration; using Content.Shared.Administration.Components; -using Content.Shared.Administration.Systems; using Content.Shared.Atmos.Components; using Content.Shared.Body; using Content.Shared.Body.Components; diff --git a/Content.Server/Tabletop/Components/TabletopGameComponent.cs b/Content.Server/Tabletop/Components/TabletopGameComponent.cs deleted file mode 100644 index da7a09b213..0000000000 --- a/Content.Server/Tabletop/Components/TabletopGameComponent.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Numerics; - -namespace Content.Server.Tabletop.Components -{ - /// - /// A component that makes an object playable as a tabletop game. - /// - [RegisterComponent, Access(typeof(TabletopSystem))] - public sealed partial class TabletopGameComponent : Component - { - /// - /// The localized name of the board. Shown in the UI. - /// - [DataField] - public LocId BoardName { get; private set; } = "tabletop-default-board-name"; - - /// - /// The type of method used to set up a tabletop. - /// - [DataField(required: true)] - public TabletopSetup Setup { get; private set; } = new TabletopChessSetup(); - - /// - /// The size of the viewport being opened. Must match the board dimensions otherwise you'll get the space parallax (unless that's what you want). - /// - [DataField] - public Vector2i Size { get; private set; } = (300, 300); - - /// - /// The zoom of the viewport camera. - /// - [DataField] - public Vector2 CameraZoom { get; private set; } = Vector2.One; - - /// - /// The specific session of this tabletop. - /// - [ViewVariables] - public TabletopSession? Session { get; set; } = null; - } -} diff --git a/Content.Server/Tabletop/Components/TabletopGamerComponent.cs b/Content.Server/Tabletop/Components/TabletopGamerComponent.cs deleted file mode 100644 index 406dcc2853..0000000000 --- a/Content.Server/Tabletop/Components/TabletopGamerComponent.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Content.Server.Tabletop.Components -{ - /// - /// Component for marking an entity as currently playing a tabletop. - /// - [RegisterComponent, Access(typeof(TabletopSystem))] - public sealed partial class TabletopGamerComponent : Component - { - [DataField("tabletop")] - public EntityUid Tabletop { get; set; } = EntityUid.Invalid; - } -} diff --git a/Content.Server/Tabletop/TabletopBackgammonSetup.cs b/Content.Server/Tabletop/TabletopBackgammonSetup.cs deleted file mode 100644 index 4833819b69..0000000000 --- a/Content.Server/Tabletop/TabletopBackgammonSetup.cs +++ /dev/null @@ -1,67 +0,0 @@ -using JetBrains.Annotations; - -namespace Content.Server.Tabletop -{ - [UsedImplicitly] - public sealed partial class TabletopBackgammonSetup : TabletopSetup - { - - [DataField("whitePiecePrototype")] - public string WhitePiecePrototype { get; private set; } = "WhiteTabletopPiece"; - - [DataField("blackPiecePrototype")] - public string BlackPiecePrototype { get; private set; } = "BlackTabletopPiece"; - public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) - { - var board = entityManager.SpawnEntity(BoardPrototype, session.Position); - - const float borderLengthX = 7.35f; //BORDER - const float borderLengthY = 5.60f; //BORDER - - const float boardDistanceX = 1.25f; - const float pieceDistanceY = 0.80f; - - float GetXPosition(float distanceFromSide, bool isLeftSide) - { - var pos = borderLengthX - (distanceFromSide * boardDistanceX); - return isLeftSide ? -pos : pos; - } - - float GetYPosition(float positionNumber, bool isTop) - { - var pos = borderLengthY - (pieceDistanceY * positionNumber); - return isTop ? pos : -pos; - } - - void AddPieces( - float distanceFromSide, - int numberOfPieces, - bool isBlackPiece, - bool isTop, - bool isLeftSide) - { - for (int i = 0; i < numberOfPieces; i++) - { - session.Entities.Add(entityManager.SpawnEntity(isBlackPiece ? BlackPiecePrototype : WhitePiecePrototype, session.Position.Offset(GetXPosition(distanceFromSide, isLeftSide), GetYPosition(i, isTop)))); - } - } - - // Top left - AddPieces(0, 5, true, true, true); - // top middle left - AddPieces(4, 3, false, true, true); - // top middle right - AddPieces(5, 5, false, true, false); - // top far right - AddPieces(0, 2, true, true, false); - // bottom left - AddPieces(0, 5, false, false, true); - // bottom middle left - AddPieces(4, 3, true, false, true); - // bottom middle right - AddPieces(5, 5, true, false, false); - // bottom far right - AddPieces(0, 2, false, false, false); - } - } -} diff --git a/Content.Server/Tabletop/TabletopCheckerSetup.cs b/Content.Server/Tabletop/TabletopCheckerSetup.cs deleted file mode 100644 index aa38bac7a3..0000000000 --- a/Content.Server/Tabletop/TabletopCheckerSetup.cs +++ /dev/null @@ -1,104 +0,0 @@ -using JetBrains.Annotations; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Tabletop -{ - [UsedImplicitly] - public sealed partial class TabletopCheckerSetup : TabletopSetup - { - - [DataField("prototypePieceWhite", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string PrototypePieceWhite = default!; - - [DataField("prototypeCrownWhite", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string PrototypeCrownWhite = default!; - - [DataField("prototypePieceBlack", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string PrototypePieceBlack = default!; - - [DataField("prototypeCrownBlack", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string PrototypeCrownBlack = default!; - - public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) - { - session.Entities.Add( - entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(-1, 0)) - ); - - SpawnPieces(session, entityManager, session.Position.Offset(-4.5f, 3.5f)); - } - - private void SpawnPieces(TabletopSession session, IEntityManager entityManager, MapCoordinates left) - { - static float GetOffset(float offset) => offset * 1f /* separation */; - - Span pieces = stackalloc EntityUid[42]; - var pieceIndex = 0; - - // Pieces - for (var offsetY = 0; offsetY < 3; offsetY++) - { - var checker = offsetY % 2; - - for (var offsetX = 0; offsetX < 8; offsetX += 2) - { - // Prevents an extra piece on the middle row - if (checker + offsetX > 8) continue; - - pieces[pieceIndex] = entityManager.SpawnEntity( - PrototypePieceBlack, - left.Offset(GetOffset(offsetX + (1 - checker)), GetOffset(offsetY * -1)) - ); - pieces[pieceIndex] = entityManager.SpawnEntity( - PrototypePieceWhite, - left.Offset(GetOffset(offsetX + checker), GetOffset(offsetY - 7)) - ); - pieceIndex += 2; - } - } - - const int NumCrowns = 3; - const float Overlap = 0.25f; - const float xOffset = 9f / 32; - const float xOffsetBlack = 9 + xOffset; - const float xOffsetWhite = 8 + xOffset; - - // Crowns - for (var i = 0; i < NumCrowns; i++) - { - var step = -(Overlap * i); - pieces[pieceIndex] = entityManager.SpawnEntity( - PrototypeCrownBlack, - left.Offset(GetOffset(xOffsetBlack), GetOffset(step)) - ); - pieces[pieceIndex + 1] = entityManager.SpawnEntity( - PrototypeCrownWhite, - left.Offset(GetOffset(xOffsetWhite), GetOffset(step)) - ); - pieceIndex += 2; - } - - // Spares - for (var i = 0; i < 6; i++) - { - var step = -((Overlap * (NumCrowns + 2)) + (Overlap * i)); - pieces[pieceIndex] = entityManager.SpawnEntity( - PrototypePieceBlack, - left.Offset(GetOffset(xOffsetBlack), GetOffset(step)) - ); - pieces[pieceIndex] = entityManager.SpawnEntity( - PrototypePieceWhite, - left.Offset(GetOffset(xOffsetWhite), GetOffset(step)) - ); - pieceIndex += 2; - } - - for (var i = 0; i < pieces.Length; i++) - { - session.Entities.Add(pieces[i]); - } - } - } -} diff --git a/Content.Server/Tabletop/TabletopChessSetup.cs b/Content.Server/Tabletop/TabletopChessSetup.cs deleted file mode 100644 index b636b8c05f..0000000000 --- a/Content.Server/Tabletop/TabletopChessSetup.cs +++ /dev/null @@ -1,89 +0,0 @@ -using JetBrains.Annotations; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Tabletop -{ - [UsedImplicitly] - public sealed partial class TabletopChessSetup : TabletopSetup - { - - // TODO: Un-hardcode the rest of entity prototype IDs, probably. - - public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) - { - var chessboard = entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(-1, 0)); - - session.Entities.Add(chessboard); - - SpawnPieces(session, entityManager, session.Position.Offset(-4.5f, 3.5f)); - } - - private void SpawnPieces(TabletopSession session, IEntityManager entityManager, MapCoordinates topLeft, float separation = 1f) - { - var (mapId, x, y) = topLeft; - - // Spawn all black pieces - SpawnPiecesRow(session, entityManager, "Black", topLeft, separation); - SpawnPawns(session, entityManager, "Black", new MapCoordinates(x, y - separation, mapId) , separation); - - // Spawn all white pieces - 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 - EntityUid tempQualifier = entityManager.SpawnEntity("BlackQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 3 * separation, mapId)); - session.Entities.Add(tempQualifier); - EntityUid tempQualifier1 = entityManager.SpawnEntity("WhiteQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 4 * separation, mapId)); - session.Entities.Add(tempQualifier1); - } - - // TODO: refactor to load FEN instead - private void SpawnPiecesRow(TabletopSession session, IEntityManager entityManager, string color, MapCoordinates left, float separation = 1f) - { - const string piecesRow = "rnbqkbnr"; - - var (mapId, x, y) = left; - - for (int i = 0; i < 8; i++) - { - switch (piecesRow[i]) - { - case 'r': - EntityUid tempQualifier = entityManager.SpawnEntity(color + "Rook", new MapCoordinates(x + i * separation, y, mapId)); - session.Entities.Add(tempQualifier); - break; - case 'n': - EntityUid tempQualifier1 = entityManager.SpawnEntity(color + "Knight", new MapCoordinates(x + i * separation, y, mapId)); - session.Entities.Add(tempQualifier1); - break; - case 'b': - EntityUid tempQualifier2 = entityManager.SpawnEntity(color + "Bishop", new MapCoordinates(x + i * separation, y, mapId)); - session.Entities.Add(tempQualifier2); - break; - case 'q': - EntityUid tempQualifier3 = entityManager.SpawnEntity(color + "Queen", new MapCoordinates(x + i * separation, y, mapId)); - session.Entities.Add(tempQualifier3); - break; - case 'k': - EntityUid tempQualifier4 = entityManager.SpawnEntity(color + "King", new MapCoordinates(x + i * separation, y, mapId)); - session.Entities.Add(tempQualifier4); - break; - } - } - } - - // TODO: refactor to load FEN instead - 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++) - { - EntityUid tempQualifier = entityManager.SpawnEntity(color + "Pawn", new MapCoordinates(x + i * separation, y, mapId)); - session.Entities.Add(tempQualifier); - } - } - } -} diff --git a/Content.Server/Tabletop/TabletopEmptySetup.cs b/Content.Server/Tabletop/TabletopEmptySetup.cs deleted file mode 100644 index 58fb109df9..0000000000 --- a/Content.Server/Tabletop/TabletopEmptySetup.cs +++ /dev/null @@ -1,14 +0,0 @@ -using JetBrains.Annotations; - -namespace Content.Server.Tabletop -{ - [UsedImplicitly] - public sealed partial class TabletopEmptySetup : TabletopSetup - { - public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) - { - var board = entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(0, 0)); - session.Entities.Add(board); - } - } -} diff --git a/Content.Server/Tabletop/TabletopParchisSetup.cs b/Content.Server/Tabletop/TabletopParchisSetup.cs deleted file mode 100644 index ffc8e03364..0000000000 --- a/Content.Server/Tabletop/TabletopParchisSetup.cs +++ /dev/null @@ -1,76 +0,0 @@ -using JetBrains.Annotations; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Tabletop -{ - [UsedImplicitly] - public sealed partial class TabletopParchisSetup : TabletopSetup - { - - [DataField("redPiecePrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string RedPiecePrototype { get; private set; } = "RedTabletopPiece"; - - [DataField("greenPiecePrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string GreenPiecePrototype { get; private set; } = "GreenTabletopPiece"; - - [DataField("yellowPiecePrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string YellowPiecePrototype { get; private set; } = "YellowTabletopPiece"; - - [DataField("bluePiecePrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string BluePiecePrototype { get; private set; } = "BlueTabletopPiece"; - - public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) - { - var board = entityManager.SpawnEntity(BoardPrototype, session.Position); - - const float x1 = 6.25f; - const float x2 = 4.25f; - - const float y1 = 6.25f; - const float y2 = 4.25f; - - var center = session.Position; - - // Red pieces. - EntityUid tempQualifier = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y1)); - session.Entities.Add(tempQualifier); - EntityUid tempQualifier1 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y2)); - session.Entities.Add(tempQualifier1); - EntityUid tempQualifier2 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y1)); - session.Entities.Add(tempQualifier2); - EntityUid tempQualifier3 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y2)); - session.Entities.Add(tempQualifier3); - - // Green pieces. - EntityUid tempQualifier4 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y1)); - session.Entities.Add(tempQualifier4); - EntityUid tempQualifier5 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y2)); - session.Entities.Add(tempQualifier5); - EntityUid tempQualifier6 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y1)); - session.Entities.Add(tempQualifier6); - EntityUid tempQualifier7 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y2)); - session.Entities.Add(tempQualifier7); - - // Yellow pieces. - EntityUid tempQualifier8 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y1)); - session.Entities.Add(tempQualifier8); - EntityUid tempQualifier9 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y2)); - session.Entities.Add(tempQualifier9); - EntityUid tempQualifier10 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y1)); - session.Entities.Add(tempQualifier10); - EntityUid tempQualifier11 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y2)); - session.Entities.Add(tempQualifier11); - - // Blue pieces. - EntityUid tempQualifier12 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y1)); - session.Entities.Add(tempQualifier12); - EntityUid tempQualifier13 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y2)); - session.Entities.Add(tempQualifier13); - EntityUid tempQualifier14 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y1)); - session.Entities.Add(tempQualifier14); - EntityUid tempQualifier15 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y2)); - session.Entities.Add(tempQualifier15); - } - } -} diff --git a/Content.Server/Tabletop/TabletopSession.cs b/Content.Server/Tabletop/TabletopSession.cs deleted file mode 100644 index 5e7c6ac02e..0000000000 --- a/Content.Server/Tabletop/TabletopSession.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Numerics; -using Robust.Shared.Map; -using Robust.Shared.Player; - -namespace Content.Server.Tabletop -{ - /// - /// A class for storing data about a running tabletop game. - /// - public sealed class TabletopSession - { - /// - /// The center position of this session. - /// - public readonly MapCoordinates Position; - - /// - /// The set of players currently playing this tabletop game. - /// - public readonly Dictionary Players = new(); - - /// - /// All entities bound to this session. If you create an entity for this session, you have to add it here. - /// - public readonly HashSet Entities = new(); - - public TabletopSession(MapId tabletopMap, Vector2 position) - { - Position = new MapCoordinates(position, tabletopMap); - } - } -} diff --git a/Content.Server/Tabletop/TabletopSessionPlayerData.cs b/Content.Server/Tabletop/TabletopSessionPlayerData.cs deleted file mode 100644 index 5ffe4be362..0000000000 --- a/Content.Server/Tabletop/TabletopSessionPlayerData.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Content.Server.Tabletop -{ - /// - /// A class that stores per-player data for tabletops. - /// - public sealed class TabletopSessionPlayerData - { - public EntityUid Camera { get; set; } - } -} diff --git a/Content.Server/Tabletop/TabletopSetup.cs b/Content.Server/Tabletop/TabletopSetup.cs deleted file mode 100644 index 66aff1fca7..0000000000 --- a/Content.Server/Tabletop/TabletopSetup.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Tabletop -{ - [ImplicitDataDefinitionForInheritors] - public abstract partial class TabletopSetup - { - /// - /// 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); - - [DataField("boardPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string BoardPrototype = default!; - } -} diff --git a/Content.Server/Tabletop/TabletopSystem.Session.cs b/Content.Server/Tabletop/TabletopSystem.Session.cs index 2f271b4b66..07ef93d3b5 100644 --- a/Content.Server/Tabletop/TabletopSystem.Session.cs +++ b/Content.Server/Tabletop/TabletopSystem.Session.cs @@ -1,152 +1,143 @@ using System.Numerics; -using Content.Server.Tabletop.Components; +using Content.Shared.Tabletop; +using Content.Shared.Tabletop.Components; using Content.Shared.Tabletop.Events; using Robust.Shared.Player; using Robust.Shared.Utility; -namespace Content.Server.Tabletop +namespace Content.Server.Tabletop; + +public sealed partial class TabletopSystem { - public sealed 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. + public TabletopSession EnsureSession(TabletopGameComponent tabletop) { - /// - /// 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. - public 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); + + Log.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 (!TryComp(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) + QueueDel(euid); + + tabletop.Session = null; + } + + public override void OpenSessionFor(ICommonSession player, EntityUid uid) + { + if (!TryComp(uid, out TabletopGameComponent? tabletop) || player.AttachedEntity is not { Valid: true } 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 (TryComp(attachedEntity, out TabletopGamerComponent? gamer)) + CloseSessionFor(player, gamer.Tabletop, false); + + // Set the entity as an absolute GAMER. + EnsureComp(attachedEntity).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(GetNetEntity(uid), GetNetEntity(camera), Loc.GetString(tabletop.BoardName), tabletop.Size), player.Channel); + } + + /// + /// 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(ICommonSession player, EntityUid uid, bool removeGamerComponent = true) + { + if (!TryComp(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 && TryComp(attachedEntity, out TabletopGamerComponent? gamer)) { - // 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 invalidate this to prevent an infinite feedback from removing the component. + gamer.Tabletop = EntityUid.Invalid; - // 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); - - Log.Info($"Created tabletop session number {tabletop} at position {session.Position}."); - - return session; + // You stop being a gamer....... + RemComp(attachedEntity); } - /// - /// Cleans up a tabletop game session, deleting every entity in it. - /// - /// The UID of the tabletop game entity. - public void CleanupSession(EntityUid uid) - { - if (!TryComp(uid, out TabletopGameComponent? tabletop)) - return; + session.Players.Remove(player); + session.Entities.Remove(data.Camera); - if (tabletop.Session is not { } session) - return; + // Deleting the view subscriber automatically cleans up subscriptions, no need to do anything else. + QueueDel(data.Camera); + } - foreach (var (player, _) in session.Players) - { - CloseSessionFor(player, uid); - } + /// + /// 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, ICommonSession player, Vector2 offset = default) + { + DebugTools.AssertNotNull(tabletop.Session); - foreach (var euid in session.Entities) - { - QueueDel(euid); - } + var session = tabletop.Session!; - tabletop.Session = null; - } + // Spawn an empty entity at the coordinates. + var camera = EntityManager.SpawnEntity(null, session.Position.Offset(offset)); - /// - /// 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(ICommonSession player, EntityUid uid) - { - if (!TryComp(uid, out TabletopGameComponent? tabletop) || player.AttachedEntity is not {Valid: true} attachedEntity) - return; + // Add an eye component and disable FOV. + var eyeComponent = EnsureComp(camera); + _eye.SetDrawFov(camera, false, eyeComponent); + _eye.SetZoom(camera, tabletop.CameraZoom, eyeComponent); - // Make sure we have a session, and add the player to it if not added already. - var session = EnsureSession(tabletop); + // Add the user to the view subscribers. If there is no player session, just skip this step. + _viewSubscriberSystem.AddViewSubscriber(camera, player); - if (session.Players.ContainsKey(player)) - return; - - if(TryComp(attachedEntity, out TabletopGamerComponent? gamer)) - CloseSessionFor(player, gamer.Tabletop, false); - - // Set the entity as an absolute GAMER. - EnsureComp(attachedEntity).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(GetNetEntity(uid), GetNetEntity(camera), Loc.GetString(tabletop.BoardName), tabletop.Size), player.Channel); - } - - /// - /// 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(ICommonSession player, EntityUid uid, bool removeGamerComponent = true) - { - if (!TryComp(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 && TryComp(attachedEntity, out TabletopGamerComponent? gamer)) - { - // We invalidate this to prevent an infinite feedback from removing the component. - gamer.Tabletop = EntityUid.Invalid; - - // You stop being a gamer....... - RemComp(attachedEntity); - } - - session.Players.Remove(player); - session.Entities.Remove(data.Camera); - - // Deleting the view subscriber automatically cleans up subscriptions, no need to do anything else. - QueueDel(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, ICommonSession 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 = EnsureComp(camera); - _eye.SetDrawFov(camera, false, eyeComponent); - _eye.SetZoom(camera, tabletop.CameraZoom, eyeComponent); - - // Add the user to the view subscribers. If there is no player session, just skip this step - _viewSubscriberSystem.AddViewSubscriber(camera, player); - - return camera; - } + return camera; } } diff --git a/Content.Server/Tabletop/TabletopSystem.cs b/Content.Server/Tabletop/TabletopSystem.cs index f83fca47be..d2314a99d1 100644 --- a/Content.Server/Tabletop/TabletopSystem.cs +++ b/Content.Server/Tabletop/TabletopSystem.cs @@ -1,202 +1,105 @@ -using Content.Server.Hands.Systems; -using Content.Server.Popups; -using Content.Server.Tabletop.Components; -using Content.Shared.CCVar; -using Content.Shared.Hands.Components; +using JetBrains.Annotations; using Content.Shared.Interaction; -using Content.Shared.Item; using Content.Shared.Tabletop; using Content.Shared.Tabletop.Components; using Content.Shared.Tabletop.Events; -using Content.Shared.Verbs; -using JetBrains.Annotations; using Robust.Server.GameObjects; -using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Player; -using Robust.Shared.Utility; -namespace Content.Server.Tabletop +namespace Content.Server.Tabletop; + +[UsedImplicitly] +public sealed partial class TabletopSystem : SharedTabletopSystem { - [UsedImplicitly] - public sealed partial class TabletopSystem : SharedTabletopSystem + [Dependency] private EyeSystem _eye = default!; + [Dependency] private SharedMapSystem _map = default!; + [Dependency] private ViewSubscriberSystem _viewSubscriberSystem = default!; + + public override void Initialize() { - [Dependency] private SharedMapSystem _map = default!; - [Dependency] private EyeSystem _eye = default!; - [Dependency] private HandsSystem _hands = default!; - [Dependency] private ViewSubscriberSystem _viewSubscriberSystem = default!; - [Dependency] private PopupSystem _popupSystem = default!; - [Dependency] private IConfigurationManager _cfg = default!; + base.Initialize(); - public override void Initialize() + SubscribeNetworkEvent(OnStopPlaying); + + SubscribeLocalEvent(OnTabletopActivate); + SubscribeLocalEvent(OnGameShutdown); + SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnGamerShutdown); + + InitializeMap(); + } + + protected override void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventArgs args) + { + if (args.SenderSession is not { } playerSession) + return; + + if (!TryComp(GetEntity(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; + + base.OnTabletopMove(msg, args); + } + + private void OnTabletopActivate(Entity ent, ref ActivateInWorldEvent args) + { + if (args.Handled || !args.Complex) + return; + + // Check that a player is attached to the entity. + if (!TryComp(args.User, out ActorComponent? actor)) + return; + + OpenSessionFor(actor.PlayerSession, ent.Owner); + } + + private void OnGameShutdown(Entity ent, ref ComponentShutdown args) + { + CleanupSession(ent.Owner); + } + + private void OnStopPlaying(TabletopStopPlayingEvent msg, EntitySessionEventArgs args) + { + CloseSessionFor(args.SenderSession, GetEntity(msg.TableUid)); + } + + private void OnPlayerDetached(Entity ent, ref PlayerDetachedEvent args) + { + if (ent.Comp.Tabletop.IsValid()) + CloseSessionFor(args.Player, ent.Comp.Tabletop); + } + + private void OnGamerShutdown(Entity ent, ref ComponentShutdown args) + { + if (!TryComp(ent.Owner, out ActorComponent? actor)) + return; + + if (ent.Comp.Tabletop.IsValid()) + CloseSessionFor(actor.PlayerSession, ent.Comp.Tabletop); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var gamer)) { - base.Initialize(); - SubscribeNetworkEvent(OnStopPlaying); - SubscribeLocalEvent(OnTabletopActivate); - SubscribeLocalEvent(OnGameShutdown); - SubscribeLocalEvent(OnPlayerDetached); - SubscribeLocalEvent(OnGamerShutdown); - SubscribeLocalEvent>(AddPlayGameVerb); - SubscribeLocalEvent(OnInteractUsing); + if (!Exists(gamer.Tabletop)) + continue; - SubscribeNetworkEvent(OnTabletopRequestTakeOut); - - InitializeMap(); - } - - private void OnTabletopRequestTakeOut(TabletopRequestTakeOut msg, EntitySessionEventArgs args) - { - if (args.SenderSession is not { } playerSession) - return; - - var table = GetEntity(msg.TableUid); - - if (!TryComp(table, out TabletopGameComponent? tabletop) || tabletop.Session is not { } session) - return; - - if (!msg.Entity.IsValid()) - return; - - var entity = GetEntity(msg.Entity); - - if (!TryComp(entity, out TabletopHologramComponent? hologram)) - { - _popupSystem.PopupEntity(Loc.GetString("tabletop-error-remove-non-hologram"), table, args.SenderSession); - return; - } - - // Check if player is actually playing at this table - if (!session.Players.ContainsKey(playerSession)) - return; - - // Find the entity, remove it from the session and set it's position to the tabletop - session.Entities.TryGetValue(entity, out var result); - session.Entities.Remove(result); - QueueDel(result); - } - - private void OnInteractUsing(EntityUid uid, TabletopGameComponent component, InteractUsingEvent args) - { - if (!_cfg.GetCVar(CCVars.GameTabletopPlace)) - return; - - if (!TryComp(args.User, out HandsComponent? hands)) - return; - - if (component.Session is not { } session) - return; - - if (!_hands.TryGetActiveItem(uid, out var handEnt)) - return; - - if (!TryComp(handEnt, out var item)) - return; - - var meta = MetaData(handEnt.Value); - var protoId = meta.EntityPrototype?.ID; - - var hologram = Spawn(protoId, session.Position.Offset(-1, 0)); - - // Make sure the entity can be dragged and can be removed, move it into the board game world and add it to the Entities hashmap - EnsureComp(hologram); - EnsureComp(hologram); - session.Entities.Add(hologram); - - _popupSystem.PopupEntity(Loc.GetString("tabletop-added-piece"), uid, args.User); - } - - protected override void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventArgs args) - { - if (args.SenderSession is not { } playerSession) - return; - - if (!TryComp(GetEntity(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; - - base.OnTabletopMove(msg, args); - } - - /// - /// Add a verb that allows the player to start playing a tabletop game. - /// - private void AddPlayGameVerb(EntityUid uid, TabletopGameComponent component, GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract) - return; - - if (!TryComp(args.User, out ActorComponent? actor)) - return; - - var playVerb = new ActivationVerb() - { - Text = Loc.GetString("tabletop-verb-play-game"), - Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/die.svg.192dpi.png")), - Act = () => OpenSessionFor(actor.PlayerSession, uid) - }; - - args.Verbs.Add(playVerb); - } - - private void OnTabletopActivate(EntityUid uid, TabletopGameComponent component, ActivateInWorldEvent args) - { - if (args.Handled || !args.Complex) - return; - - // Check that a player is attached to the entity. - if (!TryComp(args.User, out ActorComponent? actor)) - return; - - OpenSessionFor(actor.PlayerSession, uid); - } - - private void OnGameShutdown(EntityUid uid, TabletopGameComponent component, ComponentShutdown args) - { - CleanupSession(uid); - } - - private void OnStopPlaying(TabletopStopPlayingEvent msg, EntitySessionEventArgs args) - { - CloseSessionFor(args.SenderSession, GetEntity(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 (!TryComp(uid, out ActorComponent? actor)) - return; - - if(component.Tabletop.IsValid()) - CloseSessionFor(actor.PlayerSession, component.Tabletop); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var gamer)) { - if (!Exists(gamer.Tabletop)) - continue; - - if (!TryComp(uid, out ActorComponent? actor)) - { - RemComp(uid); - return; - } - - if (actor.PlayerSession.Status != SessionStatus.InGame || !CanSeeTable(uid, gamer.Tabletop)) - CloseSessionFor(actor.PlayerSession, gamer.Tabletop); + RemCompDeferred(uid); + continue; } + + if (actor.PlayerSession.Status != SessionStatus.InGame || !CanSeeTable(uid, gamer.Tabletop)) + CloseSessionFor(actor.PlayerSession, gamer.Tabletop); } } } diff --git a/Content.Shared/Tabletop/Components/TabletopGameComponent.cs b/Content.Shared/Tabletop/Components/TabletopGameComponent.cs new file mode 100644 index 0000000000..18ef7f55c8 --- /dev/null +++ b/Content.Shared/Tabletop/Components/TabletopGameComponent.cs @@ -0,0 +1,42 @@ +using System.Numerics; +using Robust.Shared.GameStates; + +namespace Content.Shared.Tabletop.Components; + +/// +/// A component that makes an object playable as a tabletop game. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedTabletopSystem))] +public sealed partial class TabletopGameComponent : Component +{ + /// + /// The localized name of the board. Shown in the UI. + /// + [DataField] + public LocId BoardName { get; private set; } = "tabletop-default-board-name"; + + /// + /// The type of method used to set up a tabletop. + /// + [DataField(required: true)] + public TabletopSetup Setup { get; private set; } = new TabletopChessSetup(); + + /// + /// The size of the viewport being opened. Must match the board dimensions otherwise you'll get the space parallax (unless that's what you want). + /// + [DataField] + public Vector2i Size { get; private set; } = (300, 300); + + /// + /// The zoom of the viewport camera. + /// + [DataField] + public Vector2 CameraZoom { get; private set; } = Vector2.One; + + /// + /// The specific session of this tabletop. + /// + [ViewVariables] + public TabletopSession? Session { get; set; } = null; +} diff --git a/Content.Shared/Tabletop/Components/TabletopGamerComponent.cs b/Content.Shared/Tabletop/Components/TabletopGamerComponent.cs new file mode 100644 index 0000000000..10097b3eca --- /dev/null +++ b/Content.Shared/Tabletop/Components/TabletopGamerComponent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Tabletop.Components; + +/// +/// Component for marking an entity as currently playing a tabletop. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedTabletopSystem))] +public sealed partial class TabletopGamerComponent : Component +{ + [DataField] + public EntityUid Tabletop = EntityUid.Invalid; +} diff --git a/Content.Server/Tabletop/TabletopHologramComponent.cs b/Content.Shared/Tabletop/Components/TabletopHologramComponent.cs similarity index 60% rename from Content.Server/Tabletop/TabletopHologramComponent.cs rename to Content.Shared/Tabletop/Components/TabletopHologramComponent.cs index e2aa65f2dc..866a86ffa3 100644 --- a/Content.Server/Tabletop/TabletopHologramComponent.cs +++ b/Content.Shared/Tabletop/Components/TabletopHologramComponent.cs @@ -1,10 +1,9 @@ -namespace Content.Server.Tabletop; +using Robust.Shared.GameStates; + +namespace Content.Shared.Tabletop.Components; /// /// This is used for tracking pieces that are simply "holograms" shown on the tabletop /// -[RegisterComponent] -public sealed partial class TabletopHologramComponent : Component -{ - -} +[RegisterComponent, NetworkedComponent] +public sealed partial class TabletopHologramComponent : Component; diff --git a/Content.Shared/Tabletop/SharedTabletopSystem.Session.cs b/Content.Shared/Tabletop/SharedTabletopSystem.Session.cs new file mode 100644 index 0000000000..bb6cd31563 --- /dev/null +++ b/Content.Shared/Tabletop/SharedTabletopSystem.Session.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Player; + +namespace Content.Shared.Tabletop; + +public abstract partial class SharedTabletopSystem +{ + /// + /// 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 virtual void OpenSessionFor(ICommonSession player, EntityUid uid) { } +} diff --git a/Content.Shared/Tabletop/SharedTabletopSystem.cs b/Content.Shared/Tabletop/SharedTabletopSystem.cs index 4e1dd6e77b..786ce3a34e 100644 --- a/Content.Shared/Tabletop/SharedTabletopSystem.cs +++ b/Content.Shared/Tabletop/SharedTabletopSystem.cs @@ -1,123 +1,216 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; using Content.Shared.ActionBlocker; +using Content.Shared.CCVar; using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; +using Content.Shared.Item; +using Content.Shared.Popups; using Content.Shared.Tabletop.Components; using Content.Shared.Tabletop.Events; +using Content.Shared.Verbs; +using Robust.Shared.Configuration; using Robust.Shared.Network; +using Robust.Shared.Player; using Robust.Shared.Serialization; +using Robust.Shared.Utility; -namespace Content.Shared.Tabletop +namespace Content.Shared.Tabletop; + +public abstract partial class SharedTabletopSystem : EntitySystem { - public abstract partial class SharedTabletopSystem : EntitySystem + [Dependency] private ActionBlockerSystem _actionBlocker = default!; + [Dependency] private IConfigurationManager _cfg = default!; + [Dependency] private SharedAppearanceSystem _appearance = default!; + [Dependency] private SharedHandsSystem _hands = default!; + [Dependency] private SharedInteractionSystem _interaction = default!; + [Dependency] private SharedMapSystem _map = default!; + [Dependency] private SharedPopupSystem _popup = default!; + [Dependency] private SharedTransformSystem _transforms = default!; + + public override void Initialize() { - [Dependency] protected ActionBlockerSystem ActionBlockerSystem = default!; - [Dependency] private SharedInteractionSystem _interactionSystem = default!; - [Dependency] private SharedAppearanceSystem _appearance = default!; - [Dependency] private SharedMapSystem _mapSystem = default!; - [Dependency] protected SharedTransformSystem Transforms = default!; + base.Initialize(); - public override void Initialize() - { - SubscribeAllEvent(OnDraggingPlayerChanged); - SubscribeAllEvent(OnTabletopMove); - } + SubscribeAllEvent(OnDraggingPlayerChanged); - /// - /// Move an entity which is dragged by the user, but check if they are allowed to do so and to these coordinates - /// - protected virtual void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventArgs args) - { - if (args.SenderSession is not { AttachedEntity: { } playerEntity } playerSession) - return; + SubscribeLocalEvent>(AddPlayGameVerb); + SubscribeLocalEvent(OnInteractUsing); - var table = GetEntity(msg.TableUid); - var moved = GetEntity(msg.MovedEntityUid); + SubscribeNetworkEvent(OnTabletopRequestTakeOut); - if (!CanSeeTable(playerEntity, table) || !CanDrag(playerEntity, moved, out _)) - return; - - // 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 = Comp(moved); - Transforms.SetParent(moved, transform, _mapSystem.GetMapOrInvalid(transform.MapID)); - Transforms.SetLocalPositionNoLerp(moved, msg.Coordinates.Position, transform); - } - - private void OnDraggingPlayerChanged(TabletopDraggingPlayerChangedEvent msg, EntitySessionEventArgs args) - { - var dragged = GetEntity(msg.DraggedEntityUid); - - if (!TryComp(dragged, out TabletopDraggableComponent? draggableComponent)) - return; - - draggableComponent.DraggingPlayer = msg.IsDragging ? args.SenderSession.UserId : null; - Dirty(dragged, draggableComponent); - - if (!TryComp(dragged, out AppearanceComponent? appearance)) - return; - - if (draggableComponent.DraggingPlayer != null) - { - _appearance.SetData(dragged, TabletopItemVisuals.Scale, new Vector2(1.25f, 1.25f), appearance); - _appearance.SetData(dragged, TabletopItemVisuals.DrawDepth, (int) DrawDepth.DrawDepth.Items + 1, appearance); - } - else - { - _appearance.SetData(dragged, TabletopItemVisuals.Scale, Vector2.One, appearance); - _appearance.SetData(dragged, TabletopItemVisuals.DrawDepth, (int) DrawDepth.DrawDepth.Items, appearance); - } - } - - - [Serializable, NetSerializable] - public sealed class TabletopDraggableComponentState : ComponentState - { - public NetUserId? DraggingPlayer; - - public TabletopDraggableComponentState(NetUserId? draggingPlayer) - { - DraggingPlayer = draggingPlayer; - } - } - - [Serializable, NetSerializable] - public sealed class TabletopRequestTakeOut : EntityEventArgs - { - public NetEntity Entity; - public NetEntity TableUid; - } - - #region Utility - - /// - /// Whether the table exists, and the player can interact with it. - /// - /// The player entity to check. - /// The table entity to check. - protected bool CanSeeTable(EntityUid playerEntity, EntityUid? table) - { - // Table may have been deleted, hence TryComp - if (!TryComp(table, out MetaDataComponent? meta) - || meta.EntityLifeStage >= EntityLifeStage.Terminating - || (meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) - { - return false; - } - - return _interactionSystem.InRangeUnobstructed(playerEntity, table.Value) && ActionBlockerSystem.CanInteract(playerEntity, table); - } - - protected bool CanDrag(EntityUid playerEntity, EntityUid target, [NotNullWhen(true)] out TabletopDraggableComponent? draggable) - { - if (!TryComp(target, out draggable)) - return false; - - // CanSeeTable checks interaction action blockers. So no need to check them here. - // If this ever changes, so that ghosts can spectate games, then the check needs to be moved here. - - return TryComp(playerEntity, out HandsComponent? hands) && hands.Hands.Count > 0; - } - #endregion + SubscribeAllEvent(OnTabletopMove); } + + private void OnTabletopRequestTakeOut(TabletopRequestTakeOut msg, EntitySessionEventArgs args) + { + if (args.SenderSession is not { } playerSession) + return; + + var table = GetEntity(msg.TableUid); + + if (!TryComp(table, out TabletopGameComponent? tabletop) || tabletop.Session is not { } session) + return; + + if (!msg.Entity.IsValid()) + return; + + var entity = GetEntity(msg.Entity); + + if (!HasComp(entity)) + { + _popup.PopupEntity(Loc.GetString("tabletop-error-remove-non-hologram"), table, args.SenderSession); + return; + } + + // Check if player is actually playing at this table. + if (!session.Players.ContainsKey(playerSession)) + return; + + // Find the entity, remove it from the session and set it's position to the tabletop. + session.Entities.TryGetValue(entity, out var result); + session.Entities.Remove(result); + PredictedQueueDel(result); + } + + private void OnInteractUsing(Entity ent, ref InteractUsingEvent args) + { + if (!_cfg.GetCVar(CCVars.GameTabletopPlace)) + return; + + if (!HasComp(args.User)) + return; + + if (ent.Comp.Session is not { } session) + return; + + if (!_hands.TryGetActiveItem(ent.Owner, out var handEnt)) + return; + + if (!HasComp(handEnt)) + return; + + var meta = MetaData(handEnt.Value); + var protoId = meta.EntityPrototype?.ID; + + var hologram = EntityManager.PredictedSpawn(protoId, session.Position.Offset(-1, 0)); + + // Make sure the entity can be dragged and can be removed, move it into the board game world and add it to the Entities hashmap. + EnsureComp(hologram); + EnsureComp(hologram); + session.Entities.Add(hologram); + + _popup.PopupClient(Loc.GetString("tabletop-added-piece"), ent.Owner, args.User); + } + + /// + /// Add a verb that allows the player to start playing a tabletop game. + /// + private void AddPlayGameVerb(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + if (!TryComp(args.User, out ActorComponent? actor)) + return; + + var playVerb = new ActivationVerb() + { + Text = Loc.GetString("tabletop-verb-play-game"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/die.svg.192dpi.png")), + Act = () => OpenSessionFor(actor.PlayerSession, ent.Owner) + }; + + args.Verbs.Add(playVerb); + } + + /// + /// Move an entity which is dragged by the user, but check if they are allowed to do so and to these coordinates. + /// + protected virtual void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventArgs args) + { + if (args.SenderSession is not { AttachedEntity: { } playerEntity }) + return; + + var table = GetEntity(msg.TableUid); + var moved = GetEntity(msg.MovedEntityUid); + + if (!CanSeeTable(playerEntity, table) || !CanDrag(playerEntity, moved, out _)) + return; + + // 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 = Comp(moved); + _transforms.SetParent(moved, transform, _map.GetMapOrInvalid(transform.MapID)); + _transforms.SetLocalPosition(moved, msg.Coordinates.Position, transform); + } + + private void OnDraggingPlayerChanged(TabletopDraggingPlayerChangedEvent msg, EntitySessionEventArgs args) + { + var dragged = GetEntity(msg.DraggedEntityUid); + + if (!TryComp(dragged, out TabletopDraggableComponent? draggableComponent)) + return; + + draggableComponent.DraggingPlayer = msg.IsDragging ? args.SenderSession.UserId : null; + Dirty(dragged, draggableComponent); + + if (!TryComp(dragged, out AppearanceComponent? appearance)) + return; + + if (draggableComponent.DraggingPlayer != null) + { + _appearance.SetData(dragged, TabletopItemVisuals.Scale, new Vector2(1.25f, 1.25f), appearance); + _appearance.SetData(dragged, TabletopItemVisuals.DrawDepth, (int)DrawDepth.DrawDepth.Items + 1, appearance); + } + else + { + _appearance.SetData(dragged, TabletopItemVisuals.Scale, Vector2.One, appearance); + _appearance.SetData(dragged, TabletopItemVisuals.DrawDepth, (int)DrawDepth.DrawDepth.Items, appearance); + } + } + + [Serializable, NetSerializable] + public sealed class TabletopDraggableComponentState(NetUserId? draggingPlayer) : ComponentState + { + public NetUserId? DraggingPlayer = draggingPlayer; + } + + [Serializable, NetSerializable] + public sealed class TabletopRequestTakeOut : EntityEventArgs + { + public NetEntity Entity; + public NetEntity TableUid; + } + + #region Utility + + /// + /// Whether the table exists, and the player can interact with it. + /// + /// The player entity to check. + /// The table entity to check. + protected bool CanSeeTable(EntityUid playerEntity, EntityUid? table) + { + // Table may have been deleted, hence TryComp. + if (!TryComp(table, out MetaDataComponent? meta) + || meta.EntityLifeStage >= EntityLifeStage.Terminating + || (meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) + { + return false; + } + + return _interaction.InRangeUnobstructed(playerEntity, table.Value) && _actionBlocker.CanInteract(playerEntity, table); + } + + protected bool CanDrag(EntityUid playerEntity, EntityUid target, [NotNullWhen(true)] out TabletopDraggableComponent? draggable) + { + if (!TryComp(target, out draggable)) + return false; + + // CanSeeTable checks interaction action blockers. So no need to check them here. + // If this ever changes, so that ghosts can spectate games, then the check needs to be moved here. + return TryComp(playerEntity, out HandsComponent? hands) && hands.Hands.Count > 0; + } + #endregion } diff --git a/Content.Shared/Tabletop/TabletopBackgammonSetup.cs b/Content.Shared/Tabletop/TabletopBackgammonSetup.cs new file mode 100644 index 0000000000..fba78ddffa --- /dev/null +++ b/Content.Shared/Tabletop/TabletopBackgammonSetup.cs @@ -0,0 +1,66 @@ +using JetBrains.Annotations; + +namespace Content.Shared.Tabletop; + +[UsedImplicitly] +public sealed partial class TabletopBackgammonSetup : TabletopSetup +{ + [DataField] + public string WhitePiecePrototype { get; private set; } = "WhiteTabletopPiece"; + + [DataField] + public string BlackPiecePrototype { get; private set; } = "BlackTabletopPiece"; + + public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) + { + var board = entityManager.SpawnEntity(BoardPrototype, session.Position); + + const float borderLengthX = 7.35f; //BORDER + const float borderLengthY = 5.60f; //BORDER + + const float boardDistanceX = 1.25f; + const float pieceDistanceY = 0.80f; + + float GetXPosition(float distanceFromSide, bool isLeftSide) + { + var pos = borderLengthX - distanceFromSide * boardDistanceX; + return isLeftSide ? -pos : pos; + } + + float GetYPosition(float positionNumber, bool isTop) + { + var pos = borderLengthY - pieceDistanceY * positionNumber; + return isTop ? pos : -pos; + } + + void AddPieces( + float distanceFromSide, + int numberOfPieces, + bool isBlackPiece, + bool isTop, + bool isLeftSide) + { + for (var i = 0; i < numberOfPieces; i++) + { + session.Entities.Add(entityManager.SpawnEntity(isBlackPiece ? BlackPiecePrototype : WhitePiecePrototype, session.Position.Offset(GetXPosition(distanceFromSide, isLeftSide), GetYPosition(i, isTop)))); + } + } + + // top left + AddPieces(0, 5, true, true, true); + // top middle left + AddPieces(4, 3, false, true, true); + // top middle right + AddPieces(5, 5, false, true, false); + // top far right + AddPieces(0, 2, true, true, false); + // bottom left + AddPieces(0, 5, false, false, true); + // bottom middle left + AddPieces(4, 3, true, false, true); + // bottom middle right + AddPieces(5, 5, true, false, false); + // bottom far right + AddPieces(0, 2, false, false, false); + } +} diff --git a/Content.Shared/Tabletop/TabletopCheckerSetup.cs b/Content.Shared/Tabletop/TabletopCheckerSetup.cs new file mode 100644 index 0000000000..8a3e9d2b41 --- /dev/null +++ b/Content.Shared/Tabletop/TabletopCheckerSetup.cs @@ -0,0 +1,102 @@ +using JetBrains.Annotations; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Tabletop; + +[UsedImplicitly] +public sealed partial class TabletopCheckerSetup : TabletopSetup +{ + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string PrototypePieceWhite = default!; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string PrototypeCrownWhite = default!; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string PrototypePieceBlack = default!; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string PrototypeCrownBlack = default!; + + public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) + { + session.Entities.Add( + entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(-1, 0)) + ); + + SpawnPieces(session, entityManager, session.Position.Offset(-4.5f, 3.5f)); + } + + private void SpawnPieces(TabletopSession session, IEntityManager entityManager, MapCoordinates left) + { + static float GetOffset(float offset) => offset * 1f /* separation */; + + var pieces = new EntityUid[42]; + var pieceIndex = 0; + + // Pieces. + for (var offsetY = 0; offsetY < 3; offsetY++) + { + var checker = offsetY % 2; + + for (var offsetX = 0; offsetX < 8; offsetX += 2) + { + // Prevents an extra piece on the middle row. + if (checker + offsetX > 8) continue; + + pieces[pieceIndex] = entityManager.SpawnEntity( + PrototypePieceBlack, + left.Offset(GetOffset(offsetX + (1 - checker)), GetOffset(offsetY * -1)) + ); + pieces[pieceIndex] = entityManager.SpawnEntity( + PrototypePieceWhite, + left.Offset(GetOffset(offsetX + checker), GetOffset(offsetY - 7)) + ); + pieceIndex += 2; + } + } + + const int numCrowns = 3; + const float overlap = 0.25f; + const float xOffset = 9f / 32; + const float xOffsetBlack = 9 + xOffset; + const float xOffsetWhite = 8 + xOffset; + + // Crowns. + for (var i = 0; i < numCrowns; i++) + { + var step = -(overlap * i); + pieces[pieceIndex] = entityManager.SpawnEntity( + PrototypeCrownBlack, + left.Offset(GetOffset(xOffsetBlack), GetOffset(step)) + ); + pieces[pieceIndex + 1] = entityManager.SpawnEntity( + PrototypeCrownWhite, + left.Offset(GetOffset(xOffsetWhite), GetOffset(step)) + ); + pieceIndex += 2; + } + + // Spares. + for (var i = 0; i < 6; i++) + { + var step = -(overlap * (numCrowns + 2) + overlap * i); + pieces[pieceIndex] = entityManager.SpawnEntity( + PrototypePieceBlack, + left.Offset(GetOffset(xOffsetBlack), GetOffset(step)) + ); + pieces[pieceIndex] = entityManager.SpawnEntity( + PrototypePieceWhite, + left.Offset(GetOffset(xOffsetWhite), GetOffset(step)) + ); + pieceIndex += 2; + } + + for (var i = 0; i < pieces.Length; i++) + { + session.Entities.Add(pieces[i]); + } + } +} diff --git a/Content.Shared/Tabletop/TabletopChessSetup.cs b/Content.Shared/Tabletop/TabletopChessSetup.cs new file mode 100644 index 0000000000..00cb7e2996 --- /dev/null +++ b/Content.Shared/Tabletop/TabletopChessSetup.cs @@ -0,0 +1,85 @@ +using JetBrains.Annotations; +using Robust.Shared.Map; + +namespace Content.Shared.Tabletop; + +[UsedImplicitly] +public sealed partial class TabletopChessSetup : TabletopSetup +{ + // TODO: Un-hardcode the rest of entity prototype IDs, probably. + + public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) + { + var chessboard = entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(-1, 0)); + + session.Entities.Add(chessboard); + + SpawnPieces(session, entityManager, session.Position.Offset(-4.5f, 3.5f)); + } + + private void SpawnPieces(TabletopSession session, IEntityManager entityManager, MapCoordinates topLeft, float separation = 1f) + { + var (mapId, x, y) = topLeft; + + // Spawn all black pieces. + SpawnPiecesRow(session, entityManager, "Black", topLeft, separation); + SpawnPawns(session, entityManager, "Black", new MapCoordinates(x, y - separation, mapId), separation); + + // Spawn all white pieces. + 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. + var tempQualifier = entityManager.SpawnEntity("BlackQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 3 * separation, mapId)); + session.Entities.Add(tempQualifier); + var tempQualifier1 = entityManager.SpawnEntity("WhiteQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 4 * separation, mapId)); + session.Entities.Add(tempQualifier1); + } + + // TODO: refactor to load FEN instead + private void SpawnPiecesRow(TabletopSession session, IEntityManager entityManager, string color, MapCoordinates left, float separation = 1f) + { + const string piecesRow = "rnbqkbnr"; + + var (mapId, x, y) = left; + + for (var i = 0; i < 8; i++) + { + switch (piecesRow[i]) + { + case 'r': + var tempQualifier = entityManager.SpawnEntity(color + "Rook", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(tempQualifier); + break; + case 'n': + var tempQualifier1 = entityManager.SpawnEntity(color + "Knight", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(tempQualifier1); + break; + case 'b': + var tempQualifier2 = entityManager.SpawnEntity(color + "Bishop", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(tempQualifier2); + break; + case 'q': + var tempQualifier3 = entityManager.SpawnEntity(color + "Queen", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(tempQualifier3); + break; + case 'k': + var tempQualifier4 = entityManager.SpawnEntity(color + "King", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(tempQualifier4); + break; + } + } + } + + // TODO: refactor to load FEN instead + private void SpawnPawns(TabletopSession session, IEntityManager entityManager, string color, MapCoordinates left, float separation = 1f) + { + var (mapId, x, y) = left; + + for (var i = 0; i < 8; i++) + { + var tempQualifier = entityManager.SpawnEntity(color + "Pawn", new MapCoordinates(x + i * separation, y, mapId)); + session.Entities.Add(tempQualifier); + } + } +} diff --git a/Content.Shared/Tabletop/TabletopEmptySetup.cs b/Content.Shared/Tabletop/TabletopEmptySetup.cs new file mode 100644 index 0000000000..92f49fc561 --- /dev/null +++ b/Content.Shared/Tabletop/TabletopEmptySetup.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; + +namespace Content.Shared.Tabletop; + +[UsedImplicitly] +public sealed partial class TabletopEmptySetup : TabletopSetup +{ + public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) + { + var board = entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(0, 0)); + session.Entities.Add(board); + } +} diff --git a/Content.Shared/Tabletop/TabletopParchisSetup.cs b/Content.Shared/Tabletop/TabletopParchisSetup.cs new file mode 100644 index 0000000000..f2292c95b1 --- /dev/null +++ b/Content.Shared/Tabletop/TabletopParchisSetup.cs @@ -0,0 +1,74 @@ +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Tabletop; + +[UsedImplicitly] +public sealed partial class TabletopParchisSetup : TabletopSetup +{ + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string RedPiecePrototype = "RedTabletopPiece"; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string GreenPiecePrototype = "GreenTabletopPiece"; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string YellowPiecePrototype = "YellowTabletopPiece"; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string BluePiecePrototype = "BlueTabletopPiece"; + + public override void SetupTabletop(TabletopSession session, IEntityManager entityManager) + { + var board = entityManager.SpawnEntity(BoardPrototype, session.Position); + + const float x1 = 6.25f; + const float x2 = 4.25f; + + const float y1 = 6.25f; + const float y2 = 4.25f; + + var center = session.Position; + + // Red pieces. + var tempQualifier = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y1)); + session.Entities.Add(tempQualifier); + var tempQualifier1 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y2)); + session.Entities.Add(tempQualifier1); + var tempQualifier2 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y1)); + session.Entities.Add(tempQualifier2); + var tempQualifier3 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y2)); + session.Entities.Add(tempQualifier3); + + // Green pieces. + var tempQualifier4 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y1)); + session.Entities.Add(tempQualifier4); + var tempQualifier5 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y2)); + session.Entities.Add(tempQualifier5); + var tempQualifier6 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y1)); + session.Entities.Add(tempQualifier6); + var tempQualifier7 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y2)); + session.Entities.Add(tempQualifier7); + + // Yellow pieces. + var tempQualifier8 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y1)); + session.Entities.Add(tempQualifier8); + var tempQualifier9 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y2)); + session.Entities.Add(tempQualifier9); + var tempQualifier10 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y1)); + session.Entities.Add(tempQualifier10); + var tempQualifier11 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y2)); + session.Entities.Add(tempQualifier11); + + // Blue pieces. + var tempQualifier12 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y1)); + session.Entities.Add(tempQualifier12); + var tempQualifier13 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y2)); + session.Entities.Add(tempQualifier13); + var tempQualifier14 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y1)); + session.Entities.Add(tempQualifier14); + var tempQualifier15 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y2)); + session.Entities.Add(tempQualifier15); + } +} diff --git a/Content.Shared/Tabletop/TabletopSession.cs b/Content.Shared/Tabletop/TabletopSession.cs new file mode 100644 index 0000000000..3d0e486148 --- /dev/null +++ b/Content.Shared/Tabletop/TabletopSession.cs @@ -0,0 +1,26 @@ +using System.Numerics; +using Robust.Shared.Map; +using Robust.Shared.Player; + +namespace Content.Shared.Tabletop; + +/// +/// A class for storing data about a running tabletop game. +/// +public sealed class TabletopSession(MapId tabletopMap, Vector2 position) +{ + /// + /// The center position of this session. + /// + public readonly MapCoordinates Position = new(position, tabletopMap); + + /// + /// The set of players currently playing this tabletop game. + /// + public readonly Dictionary Players = []; + + /// + /// All entities bound to this session. If you create an entity for this session, you have to add it here. + /// + public readonly HashSet Entities = []; +} diff --git a/Content.Shared/Tabletop/TabletopSessionPlayerData.cs b/Content.Shared/Tabletop/TabletopSessionPlayerData.cs new file mode 100644 index 0000000000..94a01d81a1 --- /dev/null +++ b/Content.Shared/Tabletop/TabletopSessionPlayerData.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Tabletop; + +/// +/// A class that stores per-player data for tabletops. +/// +public sealed class TabletopSessionPlayerData +{ + public EntityUid Camera { get; set; } +} diff --git a/Content.Shared/Tabletop/TabletopSetup.cs b/Content.Shared/Tabletop/TabletopSetup.cs new file mode 100644 index 0000000000..c29dc8464c --- /dev/null +++ b/Content.Shared/Tabletop/TabletopSetup.cs @@ -0,0 +1,19 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Tabletop; + +[ImplicitDataDefinitionForInheritors] +public abstract partial class TabletopSetup +{ + /// + /// 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); + + [DataField] + public EntProtoId BoardPrototype; +}