Improves and cleans up TabletopSystem further. (#4633)

This commit is contained in:
Vera Aguilera Puerto
2021-09-19 11:07:35 +02:00
committed by GitHub
parent 08d6801ec5
commit a9b3b5136b
11 changed files with 502 additions and 242 deletions

View File

@@ -1,16 +1,19 @@
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Tabletop.Components namespace Content.Server.Tabletop.Components
{ {
/// <summary> /// <summary>
/// A component that makes an object playable as a tabletop game. /// A component that makes an object playable as a tabletop game.
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent, Friend(typeof(TabletopSystem))]
public class TabletopGameComponent : Component public class TabletopGameComponent : Component
{ {
public override string Name => "TabletopGame"; public override string Name => "TabletopGame";
@@ -27,6 +30,9 @@ namespace Content.Server.Tabletop.Components
[DataField("cameraZoom")] [DataField("cameraZoom")]
public Vector2 CameraZoom { get; } = Vector2.One; public Vector2 CameraZoom { get; } = Vector2.One;
[ViewVariables]
public TabletopSession? Session { get; set; } = null;
/// <summary> /// <summary>
/// A verb that allows the player to start playing a tabletop game. /// A verb that allows the player to start playing a tabletop game.
/// </summary> /// </summary>
@@ -35,7 +41,7 @@ namespace Content.Server.Tabletop.Components
{ {
protected override void GetData(IEntity user, TabletopGameComponent component, VerbData data) protected override void GetData(IEntity user, TabletopGameComponent component, VerbData data)
{ {
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user)) if (!user.HasComponent<ActorComponent>() || !EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))
{ {
data.Visibility = VerbVisibility.Invisible; data.Visibility = VerbVisibility.Invisible;
return; return;
@@ -47,7 +53,8 @@ namespace Content.Server.Tabletop.Components
protected override void Activate(IEntity user, TabletopGameComponent component) protected override void Activate(IEntity user, TabletopGameComponent component)
{ {
EntitySystem.Get<TabletopSystem>().OpenTable(user, component.Owner); if(user.TryGetComponent(out ActorComponent? actor))
EntitySystem.Get<TabletopSystem>().OpenSessionFor(actor.PlayerSession, component.Owner.Uid);
} }
} }
} }

View File

@@ -0,0 +1,18 @@
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Tabletop.Components
{
/// <summary>
/// Component for marking an entity as currently playing a tabletop.
/// </summary>
[RegisterComponent, Friend(typeof(TabletopSystem))]
public class TabletopGamerComponent : Component
{
public override string Name => "TabletopGamer";
[DataField("tabletop")]
public EntityUid Tabletop { get; set; } = EntityUid.Invalid;
}
}

View File

@@ -1,9 +1,11 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Tabletop namespace Content.Server.Tabletop
{ {
[UsedImplicitly]
public class TabletopChessSetup : TabletopSetup public class TabletopChessSetup : TabletopSetup
{ {
[DataField("boardPrototype")] [DataField("boardPrototype")]
@@ -11,33 +13,34 @@ namespace Content.Server.Tabletop
// TODO: Un-hardcode the rest of entity prototype IDs, probably. // 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)); var chessboard = entityManager.SpawnEntity(ChessBoardPrototype, session.Position.Offset(-1, 0));
chessboard.Transform.Anchored = true;
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; var (mapId, x, y) = topLeft;
// Spawn all black pieces // Spawn all black pieces
SpawnPiecesRow(entityManager, "Black", topLeft, separation); SpawnPiecesRow(session, entityManager, "Black", topLeft, separation);
SpawnPawns(entityManager, "Black", new MapCoordinates(x, y - separation, mapId) , separation); SpawnPawns(session, entityManager, "Black", new MapCoordinates(x, y - separation, mapId) , separation);
// Spawn all white pieces // Spawn all white pieces
SpawnPawns(entityManager, "White", new MapCoordinates(x, y - 6 * separation, mapId) , separation); SpawnPawns(session, entityManager, "White", new MapCoordinates(x, y - 6 * separation, mapId) , separation);
SpawnPiecesRow(entityManager, "White", new MapCoordinates(x, y - 7 * separation, mapId), separation); SpawnPiecesRow(session, entityManager, "White", new MapCoordinates(x, y - 7 * separation, mapId), separation);
// Extra queens // Extra queens
entityManager.SpawnEntity("BlackQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 3 * separation, mapId)); session.Entities.Add(entityManager.SpawnEntity("BlackQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 3 * separation, mapId)).Uid);
entityManager.SpawnEntity("WhiteQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 4 * separation, mapId)); session.Entities.Add(entityManager.SpawnEntity("WhiteQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 4 * separation, mapId)).Uid);
} }
// TODO: refactor to load FEN instead // 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"; const string piecesRow = "rnbqkbnr";
@@ -48,32 +51,32 @@ namespace Content.Server.Tabletop
switch (piecesRow[i]) switch (piecesRow[i])
{ {
case 'r': 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; break;
case 'n': 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; break;
case 'b': 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; break;
case 'q': 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; break;
case 'k': 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; break;
} }
} }
} }
// TODO: refactor to load FEN instead // 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; var (mapId, x, y) = left;
for (int i = 0; i < 8; i++) 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);
} }
} }
} }

View File

@@ -1,9 +1,10 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Tabletop namespace Content.Server.Tabletop
{ {
[UsedImplicitly]
public class TabletopParchisSetup : TabletopSetup public class TabletopParchisSetup : TabletopSetup
{ {
[DataField("boardPrototype")] [DataField("boardPrototype")]
@@ -21,10 +22,9 @@ namespace Content.Server.Tabletop
[DataField("bluePiecePrototype")] [DataField("bluePiecePrototype")]
public string BluePiecePrototype { get; } = "BlueParchisPiece"; 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)); var board = entityManager.SpawnEntity(ParchisBoardPrototype, session.Position);
board.Transform.Anchored = true;
const float x1 = 6.25f; const float x1 = 6.25f;
const float x2 = 4.25f; const float x2 = 4.25f;
@@ -32,29 +32,31 @@ namespace Content.Server.Tabletop
const float y1 = 6.25f; const float y1 = 6.25f;
const float y2 = 4.25f; const float y2 = 4.25f;
var center = session.Position;
// Red pieces. // Red pieces.
entityManager.SpawnEntity(RedPiecePrototype, new MapCoordinates(-x1, -y1, mapId)); session.Entities.Add(entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y1)).Uid);
entityManager.SpawnEntity(RedPiecePrototype, new MapCoordinates(-x1, -y2, mapId)); session.Entities.Add(entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y2)).Uid);
entityManager.SpawnEntity(RedPiecePrototype, new MapCoordinates(-x2, -y1, mapId)); session.Entities.Add(entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y1)).Uid);
entityManager.SpawnEntity(RedPiecePrototype, new MapCoordinates(-x2, -y2, mapId)); session.Entities.Add(entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y2)).Uid);
// Green pieces. // Green pieces.
entityManager.SpawnEntity(GreenPiecePrototype, new MapCoordinates(x1, -y1, mapId)); session.Entities.Add(entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y1)).Uid);
entityManager.SpawnEntity(GreenPiecePrototype, new MapCoordinates(x1, -y2, mapId)); session.Entities.Add(entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y2)).Uid);
entityManager.SpawnEntity(GreenPiecePrototype, new MapCoordinates(x2, -y1, mapId)); session.Entities.Add(entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y1)).Uid);
entityManager.SpawnEntity(GreenPiecePrototype, new MapCoordinates(x2, -y2, mapId)); session.Entities.Add(entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y2)).Uid);
// Yellow pieces. // Yellow pieces.
entityManager.SpawnEntity(YellowPiecePrototype, new MapCoordinates(x1, y1, mapId)); session.Entities.Add(entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y1)).Uid);
entityManager.SpawnEntity(YellowPiecePrototype, new MapCoordinates(x1, y2, mapId)); session.Entities.Add(entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y2)).Uid);
entityManager.SpawnEntity(YellowPiecePrototype, new MapCoordinates(x2, y1, mapId)); session.Entities.Add(entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y1)).Uid);
entityManager.SpawnEntity(YellowPiecePrototype, new MapCoordinates(x2, y2, mapId)); session.Entities.Add(entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y2)).Uid);
// Blue pieces. // Blue pieces.
entityManager.SpawnEntity(BluePiecePrototype, new MapCoordinates(-x1, y1, mapId)); session.Entities.Add(entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y1)).Uid);
entityManager.SpawnEntity(BluePiecePrototype, new MapCoordinates(-x1, y2, mapId)); session.Entities.Add(entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y2)).Uid);
entityManager.SpawnEntity(BluePiecePrototype, new MapCoordinates(-x2, y1, mapId)); session.Entities.Add(entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y1)).Uid);
entityManager.SpawnEntity(BluePiecePrototype, new MapCoordinates(-x2, y2, mapId)); session.Entities.Add(entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y2)).Uid);
} }
} }
} }

View File

@@ -1,55 +1,34 @@
using System.Collections.Generic; using System.Collections.Generic;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.Tabletop namespace Content.Server.Tabletop
{ {
/// <summary> /// <summary>
/// A struct for storing data about a running tabletop game. /// A class for storing data about a running tabletop game.
/// </summary> /// </summary>
public struct TabletopSession public class TabletopSession
{ {
/// <summary> /// <summary>
/// The map ID associated with this tabletop game session. /// The center position of this session.
/// </summary> /// </summary>
public MapId MapId; public readonly MapCoordinates Position;
/// <summary> /// <summary>
/// The set of players currently playing this tabletop game. /// The set of players currently playing this tabletop game.
/// </summary> /// </summary>
private readonly HashSet<IPlayerSession> _currentPlayers; public readonly Dictionary<IPlayerSession, TabletopSessionPlayerData> Players = new();
/// <param name="mapId">The map ID associated with this tabletop game.</param>
public TabletopSession(MapId mapId)
{
MapId = mapId;
_currentPlayers = new();
}
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public bool IsPlaying(IPlayerSession playerSession) public readonly HashSet<EntityUid> Entities = new();
{
return _currentPlayers.Contains(playerSession);
}
/// <summary> public TabletopSession(MapId tabletopMap, Vector2 position)
/// Store that this player has started playing this tabletop game. If the player was already playing, nothing
/// happens.
/// </summary>
public void StartPlaying(IPlayerSession playerSession)
{ {
_currentPlayers.Add(playerSession); Position = new MapCoordinates(position, tabletopMap);
}
/// <summary>
/// Store that this player has stopped playing this tabletop game. If the player was not playing, nothing
/// happens.
/// </summary>
public void StopPlaying(IPlayerSession playerSession)
{
_currentPlayers.Remove(playerSession);
} }
} }
} }

View File

@@ -0,0 +1,12 @@
using Robust.Shared.GameObjects;
namespace Content.Server.Tabletop
{
/// <summary>
/// A class that stores per-player data for tabletops.
/// </summary>
public class TabletopSessionPlayerData
{
public EntityUid Camera { get; set; }
}
}

View File

@@ -1,5 +1,4 @@
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Tabletop namespace Content.Server.Tabletop
@@ -7,6 +6,12 @@ namespace Content.Server.Tabletop
[ImplicitDataDefinitionForInheritors] [ImplicitDataDefinitionForInheritors]
public abstract class TabletopSetup public abstract class TabletopSetup
{ {
public abstract void SetupTabletop(MapId mapId, IEntityManager entityManager); /// <summary>
/// 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.
/// </summary>
/// <param name="session">Tabletop session to set up. You'll want to grab the tabletop center position here for spawning entities.</param>
/// <param name="entityManager">Dependency that can be used for spawning entities.</param>
public abstract void SetupTabletop(TabletopSession session, IEntityManager entityManager);
} }
} }

View File

@@ -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<TabletopMoveEvent>(OnTabletopMove);
SubscribeNetworkEvent<TabletopDraggingPlayerChangedEvent>(OnDraggingPlayerChanged);
SubscribeLocalEvent<TabletopDraggableComponent, ComponentGetState>(GetDraggableState);
}
/// <summary>
/// Move an entity which is dragged by the user, but check if they are allowed to do so and to these coordinates
/// </summary>
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<TabletopDraggableComponent>(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<ITransformComponent>(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<TabletopDraggableComponent>(out var draggableComponent)) return;
draggableComponent.DraggingPlayer = msg.DraggingPlayer;
if (!draggedEntity.TryGetComponent<AppearanceComponent>(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);
}
}
}

View File

@@ -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
{
/// <summary>
/// Separation between tabletops in the tabletop map.
/// </summary>
private const int TabletopSeparation = 100;
/// <summary>
/// Map where all tabletops reside.
/// </summary>
public MapId TabletopMap { get; private set; } = MapId.Nullspace;
/// <summary>
/// The number of tabletops created in the map.
/// Used for calculating the position of the next one.
/// </summary>
private int _tabletops = 0;
/// <summary>
/// Despite the name, this method is only used to subscribe to events.
/// </summary>
private void InitializeMap()
{
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
}
/// <summary>
/// Gets the next available position for a tabletop, and increments the tabletop count.
/// </summary>
/// <returns></returns>
private Vector2 GetNextTabletopPosition()
{
return UlamSpiral(_tabletops++) * TabletopSeparation;
}
/// <summary>
/// Ensures that the tabletop map exists. Creates it if it doesn't.
/// </summary>
private void EnsureTabletopMap()
{
if (TabletopMap != MapId.Nullspace && _mapManager.MapExists(TabletopMap))
return;
TabletopMap = _mapManager.CreateMap();
_tabletops = 0;
var mapComp = ComponentManager.GetComponent<IMapComponent>(_mapManager.GetMapEntityId(TabletopMap));
// Lighting is always disabled in tabletop world.
mapComp.LightingEnabled = false;
mapComp.Dirty();
}
/// <summary>
/// Algorithm for mapping scalars to 2D positions in the same pattern as an Ulam Spiral.
/// </summary>
/// <param name="n">Scalar to map to a 2D position.</param>
/// <returns>The mapped 2D position for the scalar.</returns>
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Ensures that a <see cref="TabletopSession"/> exists on a <see cref="TabletopGameComponent"/>.
/// Creates it and sets it up if it doesn't.
/// </summary>
/// <param name="tabletop">The tabletop game in question.</param>
/// <returns>The session for the given tabletop game.</returns>
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;
}
/// <summary>
/// Cleans up a tabletop game session, deleting every entity in it.
/// </summary>
/// <param name="uid">The UID of the tabletop game entity.</param>
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;
}
/// <summary>
/// Adds a player to a tabletop game session, sending a message so the tabletop window opens on their end.
/// </summary>
/// <param name="player">The player session in question.</param>
/// <param name="uid">The UID of the tabletop game entity.</param>
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<TabletopGamerComponent>(out var gamer))
CloseSessionFor(player, gamer.Tabletop, false);
// Set the entity as an absolute GAMER.
attachedEntity.EnsureComponent<TabletopGamerComponent>().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);
}
/// <summary>
/// Removes a player from a tabletop game session, and sends them a message so their tabletop window is closed.
/// </summary>
/// <param name="player">The player in question.</param>
/// <param name="uid">The UID of the tabletop game entity.</param>
/// <param name="removeGamerComponent">Whether to remove the <see cref="TabletopGamerComponent"/> from the player's attached entity.</param>
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<TabletopGamerComponent>();
}
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);
}
/// <summary>
/// A helper method that creates a camera for a specified player, in a tabletop game session.
/// </summary>
/// <param name="tabletop">The tabletop game component in question.</param>
/// <param name="player">The player in question.</param>
/// <param name="offset">An offset from the tabletop position for the camera. Zero by default.</param>
/// <returns>The UID of the camera entity.</returns>
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>();
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;
}
}
}

View File

@@ -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.ActionBlocker;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Tabletop; using Content.Shared.Tabletop;
@@ -7,13 +6,10 @@ using Content.Shared.Tabletop.Events;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Server.Tabletop namespace Content.Server.Tabletop
{ {
@@ -24,184 +20,76 @@ namespace Content.Server.Tabletop
[Dependency] private readonly ViewSubscriberSystem _viewSubscriberSystem = default!; [Dependency] private readonly ViewSubscriberSystem _viewSubscriberSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
/// <summary>
/// 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.
/// </summary>
private readonly Dictionary<EntityUid, TabletopSession> _gameSessions = new();
public override void Initialize() public override void Initialize()
{ {
SubscribeNetworkEvent<TabletopMoveEvent>(OnTabletopMove);
SubscribeNetworkEvent<TabletopDraggingPlayerChangedEvent>(OnDraggingPlayerChanged);
SubscribeNetworkEvent<TabletopStopPlayingEvent>(OnStopPlaying); SubscribeNetworkEvent<TabletopStopPlayingEvent>(OnStopPlaying);
SubscribeLocalEvent<TabletopGameComponent, ActivateInWorldEvent>(OnTabletopActivate); SubscribeLocalEvent<TabletopGameComponent, ActivateInWorldEvent>(OnTabletopActivate);
SubscribeLocalEvent<TabletopGameComponent, ComponentShutdown>(OnGameShutdown); SubscribeLocalEvent<TabletopGameComponent, ComponentShutdown>(OnGameShutdown);
SubscribeLocalEvent<TabletopDraggableComponent, ComponentGetState>(GetCompState);
SubscribeLocalEvent<TabletopGamerComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<TabletopGamerComponent, ComponentShutdown>(OnGamerShutdown);
InitializeMap();
InitializeDraggable();
} }
private void OnTabletopActivate(EntityUid uid, TabletopGameComponent component, ActivateInWorldEvent args) 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)) 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); CleanupSession(uid);
}
/// <summary>
/// For a specific user, create a table if it does not exist yet and let the user open a UI window to play it.
/// </summary>
/// <param name="user">The user entity for which to open the window.</param>
/// <param name="table">The entity with which the tabletop game session will be associated.</param>
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);
}
/// <summary>
/// Create a session associated to this entity UID, if it does not already exist, and return it.
/// </summary>
/// <param name="uid">The entity UID to ensure a session for.</param>
/// <returns>The created/stored tabletop game session.</returns>
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<IMapComponent>();
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<TabletopDraggableComponent>(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<ITransformComponent>(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<TabletopDraggableComponent>(out var draggableComponent)) return;
draggableComponent.DraggingPlayer = msg.DraggingPlayer;
if (!draggedEntity.TryGetComponent<AppearanceComponent>(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 OnStopPlaying(TabletopStopPlayingEvent msg, EntitySessionEventArgs args) 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)
{ {
_gameSessions[msg.TableUid].StopPlaying(playerSession); if(component.Tabletop.IsValid())
} CloseSessionFor(args.Player, component.Tabletop);
} }
// TODO: needs to be refactored such that the corresponding entity on the table gets removed, instead of the whole map private void OnGamerShutdown(EntityUid uid, TabletopGamerComponent component, ComponentShutdown args)
private void OnGameShutdown(EntityUid uid, TabletopGameComponent component, ComponentShutdown args)
{ {
if (!_gameSessions.ContainsKey(uid)) return; if (!ComponentManager.TryGetComponent(uid, out ActorComponent? actor))
return;
// Delete the map and remove it from the list of sessions if(component.Tabletop.IsValid())
_mapManager.DeleteMap(_gameSessions[uid].MapId); CloseSessionFor(actor.PlayerSession, component.Tabletop);
_gameSessions.Remove(uid);
} }
#endregion public override void Update(float frameTime)
#region Utility
/// <summary>
/// Create a camera entity for a user to control, and add the user to the view subscribers.
/// </summary>
/// <param name="tabletop">The tabletop to create the camera for.</param>
/// <param name="user">The user entity to create this camera for and add to the view subscribers.</param>
/// <param name="coordinates">The map coordinates to spawn this camera at.</param>
// 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 base.Update(frameTime);
var camera = EntityManager.SpawnEntity(null, coordinates);
// Add an eye component and disable FOV foreach (var gamer in ComponentManager.EntityQuery<TabletopGamerComponent>(true))
var eyeComponent = camera.EnsureComponent<EyeComponent>();
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); if (!EntityManager.TryGetEntity(gamer.Tabletop, out var table))
} continue;
return camera; if (!gamer.Owner.TryGetComponent(out ActorComponent? actor))
} {
gamer.Owner.RemoveComponent<TabletopGamerComponent>();
return;
};
#endregion if (actor.PlayerSession.Status > SessionStatus.Connected || CanSeeTable(gamer.Owner, table)
|| !StunnedOrNoHands(gamer.Owner))
continue;
CloseSessionFor(actor.PlayerSession, table.Uid);
}
}
} }
} }