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
}
}