Files
RobustToolbox/Robust.Server/Player/PlayerManager.cs
Pieter-Jan Briers dc1464b462 High-bandwidth transfer system (#6373)
* WebSocket-based data transfer system

* Move resource downloads/uploads to the new transfer system

Should drastically increase the permitted practical size

* Transfer impl for Lidgren

* Async impl for receive stream

* Use unbounded channel for Lidgren

* Add metrics

* More comments

* Add serverside stream limit to avoid being a DoS vector

* Fix tests

* Oops forgot to actually implement sequence channels in NetMessage

* Doc comment for NetMessage.SequenceChannel

* Release notes
2026-01-19 20:44:44 +01:00

210 lines
6.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Prometheus;
using Robust.Server.Configuration;
using Robust.Server.GameObjects;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.Player
{
/// <summary>
/// This class will manage connected player sessions.
/// </summary>
internal sealed class PlayerManager : SharedPlayerManager, IPlayerManager
{
private static readonly Gauge PlayerCountMetric = Metrics
.CreateGauge("robust_player_count", "Number of players on the server.");
[Dependency] private readonly IBaseServer _baseServer = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IServerNetManager _network = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IServerNetConfigurationManager _cfg = default!;
public BoundKeyMap KeyMap { get; private set; } = default!;
/// <inheritdoc />
public override void Initialize(int maxPlayers)
{
base.Initialize(maxPlayers);
KeyMap = new BoundKeyMap(_reflectionManager);
KeyMap.PopulateKeyFunctionsMap();
_network.RegisterNetMessage<MsgPlayerListReq>(HandlePlayerListReq);
_network.RegisterNetMessage<MsgPlayerList>();
_network.RegisterNetMessage<MsgSyncTimeBase>();
_network.Connecting += OnConnecting;
_network.Connected += NewSession;
_network.Disconnect += EndSession;
}
public override void Shutdown()
{
base.Shutdown();
KeyMap = default!;
_network.Connecting -= OnConnecting;
_network.Connected -= NewSession;
_network.Disconnect -= EndSession;
}
private Task OnConnecting(NetConnectingArgs args)
{
if (PlayerCount >= _baseServer.MaxPlayers)
{
args.Deny("The server is full.");
}
return Task.CompletedTask;
}
/// <summary>
/// Creates a new session for a client.
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void NewSession(object? sender, NetChannelArgs args)
{
CreateAndAddSession(args.Channel);
PlayerCountMetric.Set(PlayerCount);
// Synchronize base time.
var msgTimeBase = new MsgSyncTimeBase();
(msgTimeBase.Time, msgTimeBase.Tick) = _timing.TimeBase;
_network.ServerSendMessage(msgTimeBase, args.Channel);
_cfg.SyncConnectingClient(args.Channel);
}
private void EndSession(object? sender, NetChannelArgs args)
{
EndSession(args.Channel.UserId);
}
/// <summary>
/// Ends a clients session, and disconnects them.
/// </summary>
internal void EndSession(NetUserId user)
{
if (!TryGetSessionById(user, out var session))
return;
RemoveSession(session.UserId);
SetStatus(session, SessionStatus.Disconnected);
SetAttachedEntity(session, null, out _, true);
var viewSys = EntManager.System<ViewSubscriberSystem>();
foreach (var eye in session.ViewSubscriptions.ToArray())
{
viewSys.RemoveViewSubscriber(eye, session);
}
PlayerCountMetric.Set(PlayerCount);
Dirty();
}
private void HandlePlayerListReq(MsgPlayerListReq message)
{
var channel = message.MsgChannel;
var session = (CommonSession) GetSessionByChannel(channel);
session.InitialPlayerListReqDone = true;
if (!session.InitialResourcesDone)
return;
SendPlayerList(channel, session);
}
public void MarkPlayerResourcesSent(INetChannel channel)
{
var session = (CommonSession) GetSessionByChannel(channel);
session.InitialResourcesDone = true;
if (!session.InitialPlayerListReqDone)
return;
SendPlayerList(channel, session);
}
private void SendPlayerList(INetChannel channel, CommonSession session)
{
var players = Sessions;
var netMsg = new MsgPlayerList();
// client session is complete, set their status accordingly.
// This is done before the packet is built, so that the client
// can see themselves Connected.
session.ConnectedTime = DateTime.UtcNow;
SetStatus(session, SessionStatus.Connected);
var list = new List<SessionState>();
foreach (var client in players)
{
var info = new SessionState
{
UserId = client.UserId,
Name = client.Name,
Status = client.Status
};
list.Add(info);
}
netMsg.Plyrs = list;
channel.SendMessage(netMsg);
}
public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
{
if (!_entityManager.TryGetComponent(uid, out ActorComponent? actor))
{
session = null;
return false;
}
session = actor.PlayerSession;
return true;
}
internal ICommonSession AddDummySession(NetUserId user, string name)
{
#if FULL_RELEASE
// Lets not make it completely trivial to fake player counts.
throw new NotSupportedException();
#endif
Lock.EnterWriteLock();
DummySession session;
try
{
UserIdMap[name] = user;
if (!PlayerData.TryGetValue(user, out var data))
PlayerData[user] = data = new(user, name);
session = new DummySession(user, name, data);
InternalSessions.Add(user, session);
}
finally
{
Lock.ExitWriteLock();
}
UpdateState(session);
return session;
}
}
}