From 23df411839151169c68e6959802343fe3bbf85df Mon Sep 17 00:00:00 2001 From: Ed <96445749+TheShuEd@users.noreply.github.com> Date: Tue, 12 May 2026 16:28:55 +0300 Subject: [PATCH 1/4] file-scoping Player manager --- Robust.Server/Player/FilterSystem.cs | 20 +- Robust.Server/Player/PlayerManager.cs | 296 +++++++++++++------------- 2 files changed, 156 insertions(+), 160 deletions(-) diff --git a/Robust.Server/Player/FilterSystem.cs b/Robust.Server/Player/FilterSystem.cs index f89ce972912..207e9f6abbe 100644 --- a/Robust.Server/Player/FilterSystem.cs +++ b/Robust.Server/Player/FilterSystem.cs @@ -1,20 +1,18 @@ -using Robust.Server.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Player; -namespace Robust.Server.Player +namespace Robust.Server.Player; + +internal sealed class FilterSystem : SharedFilterSystem { - internal sealed class FilterSystem : SharedFilterSystem + public override Filter FromEntities(Filter filter, params EntityUid[] entities) { - public override Filter FromEntities(Filter filter, params EntityUid[] entities) + foreach (var uid in entities) { - foreach (var uid in entities) - { - if (TryComp(uid, out ActorComponent? actor)) - filter.AddPlayer(actor.PlayerSession); - } - - return filter; + if (TryComp(uid, out ActorComponent? actor)) + filter.AddPlayer(actor.PlayerSession); } + + return filter; } } diff --git a/Robust.Server/Player/PlayerManager.cs b/Robust.Server/Player/PlayerManager.cs index b46f18a4bc7..07323cfc950 100644 --- a/Robust.Server/Player/PlayerManager.cs +++ b/Robust.Server/Player/PlayerManager.cs @@ -16,194 +16,192 @@ using Robust.Shared.Player; using Robust.Shared.Reflection; using Robust.Shared.Timing; -using Robust.Shared.Utility; -namespace Robust.Server.Player +namespace Robust.Server.Player; + +/// +/// This class will manage connected player sessions. +/// +internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager { - /// - /// This class will manage connected player sessions. - /// - internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager - { - private static readonly Gauge PlayerCountMetric = Metrics - .CreateGauge("robust_player_count", "Number of players on the server."); + private static readonly Gauge PlayerCountMetric = Metrics + .CreateGauge("robust_player_count", "Number of players on the server."); - [Dependency] private IBaseServer _baseServer = default!; - [Dependency] private IGameTiming _timing = default!; - [Dependency] private IServerNetManager _network = default!; - [Dependency] private IReflectionManager _reflectionManager = default!; - [Dependency] private IEntityManager _entityManager = default!; - [Dependency] private IServerNetConfigurationManager _cfg = default!; + [Dependency] private IBaseServer _baseServer = default!; + [Dependency] private IGameTiming _timing = default!; + [Dependency] private IServerNetManager _network = default!; + [Dependency] private IReflectionManager _reflectionManager = default!; + [Dependency] private IEntityManager _entityManager = default!; + [Dependency] private IServerNetConfigurationManager _cfg = default!; - public BoundKeyMap KeyMap { get; private set; } = default!; + public BoundKeyMap KeyMap { get; private set; } = default!; - /// - public override void Initialize(int maxPlayers) - { - base.Initialize(maxPlayers); - KeyMap = new BoundKeyMap(_reflectionManager); - KeyMap.PopulateKeyFunctionsMap(); + /// + public override void Initialize(int maxPlayers) + { + base.Initialize(maxPlayers); + KeyMap = new BoundKeyMap(_reflectionManager); + KeyMap.PopulateKeyFunctionsMap(); - _network.RegisterNetMessage(HandlePlayerListReq); - _network.RegisterNetMessage(); - _network.RegisterNetMessage(); + _network.RegisterNetMessage(HandlePlayerListReq); + _network.RegisterNetMessage(); + _network.RegisterNetMessage(); - _network.Connecting += OnConnecting; - _network.Connected += NewSession; - _network.Disconnect += EndSession; - } + _network.Connecting += OnConnecting; + _network.Connected += NewSession; + _network.Disconnect += EndSession; + } - public override void Shutdown() - { - base.Shutdown(); - KeyMap = default!; + public override void Shutdown() + { + base.Shutdown(); + KeyMap = default!; - _network.Connecting -= OnConnecting; - _network.Connected -= NewSession; - _network.Disconnect -= EndSession; - } + _network.Connecting -= OnConnecting; + _network.Connected -= NewSession; + _network.Disconnect -= EndSession; + } - private Task OnConnecting(NetConnectingArgs args) + private Task OnConnecting(NetConnectingArgs args) + { + if (PlayerCount >= _baseServer.MaxPlayers) { - if (PlayerCount >= _baseServer.MaxPlayers) - { - args.Deny("The server is full."); - } - - return Task.CompletedTask; + args.Deny("The server is full."); } - /// - /// Creates a new session for a client. - /// - /// - /// - 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); - } + return Task.CompletedTask; + } - private void EndSession(object? sender, NetChannelArgs args) - { - EndSession(args.Channel.UserId); - } + /// + /// Creates a new session for a client. + /// + /// + /// + 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); + } - /// - /// Ends a clients session, and disconnects them. - /// - internal void EndSession(NetUserId user) - { - if (!TryGetSessionById(user, out var session)) - return; + private void EndSession(object? sender, NetChannelArgs args) + { + EndSession(args.Channel.UserId); + } - RemoveSession(session.UserId); - SetStatus(session, SessionStatus.Disconnected); - SetAttachedEntity(session, null, out _, true); + /// + /// Ends a clients session, and disconnects them. + /// + internal void EndSession(NetUserId user) + { + if (!TryGetSessionById(user, out var session)) + return; - var viewSys = EntManager.System(); - foreach (var eye in session.ViewSubscriptions.ToArray()) - { - viewSys.RemoveViewSubscriber(eye, session); - } + RemoveSession(session.UserId); + SetStatus(session, SessionStatus.Disconnected); + SetAttachedEntity(session, null, out _, true); - PlayerCountMetric.Set(PlayerCount); - Dirty(); + var viewSys = EntManager.System(); + foreach (var eye in session.ViewSubscriptions.ToArray()) + { + viewSys.RemoveViewSubscriber(eye, session); } - private void HandlePlayerListReq(MsgPlayerListReq message) - { - var channel = message.MsgChannel; - var session = (CommonSession)GetSessionByChannel(channel); - session.InitialPlayerListReqDone = true; + PlayerCountMetric.Set(PlayerCount); + Dirty(); + } - if (!session.InitialResourcesDone) - return; + private void HandlePlayerListReq(MsgPlayerListReq message) + { + var channel = message.MsgChannel; + var session = (CommonSession)GetSessionByChannel(channel); + session.InitialPlayerListReqDone = true; - SendPlayerList(channel, session); - } + if (!session.InitialResourcesDone) + return; - public void MarkPlayerResourcesSent(INetChannel channel) - { - var session = (CommonSession)GetSessionByChannel(channel); - session.InitialResourcesDone = true; + SendPlayerList(channel, session); + } - if (!session.InitialPlayerListReqDone) - return; + public void MarkPlayerResourcesSent(INetChannel channel) + { + var session = (CommonSession)GetSessionByChannel(channel); + session.InitialResourcesDone = true; - SendPlayerList(channel, session); - } + if (!session.InitialPlayerListReqDone) + return; - private void SendPlayerList(INetChannel channel, CommonSession session) - { - var players = Sessions; - var netMsg = new MsgPlayerList(); + SendPlayerList(channel, session); + } - // 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); + private void SendPlayerList(INetChannel channel, CommonSession session) + { + var players = Sessions; + var netMsg = new MsgPlayerList(); - var list = new List(); - 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); - } + // 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); - public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) + var list = new List(); + foreach (var client in players) { - if (!_entityManager.TryGetComponent(uid, out ActorComponent? actor)) + var info = new SessionState { - session = null; - return false; - } - - session = actor.PlayerSession; - return true; + UserId = client.UserId, + Name = client.Name, + Status = client.Status + }; + list.Add(info); } + netMsg.Plyrs = list; - internal ICommonSession AddDummySession(NetUserId user, string name) + 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); + Lock.EnterWriteLock(); + DummySession session; + try + { + UserIdMap[name] = user; + if (!PlayerData.TryGetValue(user, out var data)) + PlayerData[user] = data = new(user, name); - return session; + session = new DummySession(user, name, data); + InternalSessions.Add(user, session); } + finally + { + Lock.ExitWriteLock(); + } + + UpdateState(session); + + return session; } } From 08886c757a5e2c680c5e5933381a9b9ef89734fa Mon Sep 17 00:00:00 2001 From: Ed <96445749+TheShuEd@users.noreply.github.com> Date: Tue, 12 May 2026 16:30:25 +0300 Subject: [PATCH 2/4] readonly playerManager --- Robust.Client/Player/PlayerManager.cs | 4 ++-- Robust.Server/Player/PlayerManager.cs | 12 ++++++------ Robust.Shared/Player/SharedPlayerManager.cs | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Robust.Client/Player/PlayerManager.cs b/Robust.Client/Player/PlayerManager.cs index 0bc24f35cce..e47d329e837 100644 --- a/Robust.Client/Player/PlayerManager.cs +++ b/Robust.Client/Player/PlayerManager.cs @@ -19,8 +19,8 @@ namespace Robust.Client.Player /// internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager { - [Dependency] private IClientNetManager _network = default!; - [Dependency] private IBaseClient _client = default!; + [Dependency] private readonly IClientNetManager _network = default!; + [Dependency] private readonly IBaseClient _client = default!; /// /// Received player states that had an unknown . diff --git a/Robust.Server/Player/PlayerManager.cs b/Robust.Server/Player/PlayerManager.cs index 07323cfc950..b82081ddb64 100644 --- a/Robust.Server/Player/PlayerManager.cs +++ b/Robust.Server/Player/PlayerManager.cs @@ -27,12 +27,12 @@ internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManage private static readonly Gauge PlayerCountMetric = Metrics .CreateGauge("robust_player_count", "Number of players on the server."); - [Dependency] private IBaseServer _baseServer = default!; - [Dependency] private IGameTiming _timing = default!; - [Dependency] private IServerNetManager _network = default!; - [Dependency] private IReflectionManager _reflectionManager = default!; - [Dependency] private IEntityManager _entityManager = default!; - [Dependency] private IServerNetConfigurationManager _cfg = default!; + [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!; diff --git a/Robust.Shared/Player/SharedPlayerManager.cs b/Robust.Shared/Player/SharedPlayerManager.cs index 2a78bb9e7dc..54ab6686fd9 100644 --- a/Robust.Shared/Player/SharedPlayerManager.cs +++ b/Robust.Shared/Player/SharedPlayerManager.cs @@ -11,11 +11,11 @@ namespace Robust.Shared.Player; internal abstract partial class SharedPlayerManager : ISharedPlayerManager { - [Dependency] protected IEntityManager EntManager = default!; - [Dependency] protected IComponentFactory Factory = default!; - [Dependency] protected ILogManager LogMan = default!; - [Dependency] protected IGameTiming Timing = default!; - [Dependency] private INetManager _netMan = default!; + [Dependency] protected readonly IEntityManager EntManager = default!; + [Dependency] protected readonly IComponentFactory Factory = default!; + [Dependency] protected readonly ILogManager LogMan = default!; + [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] private readonly INetManager _netMan = default!; protected ISawmill Sawmill = default!; From 65bdc7d938380d1d2382cf70229eac82292f433c Mon Sep 17 00:00:00 2001 From: Ed <96445749+TheShuEd@users.noreply.github.com> Date: Tue, 12 May 2026 16:30:39 +0300 Subject: [PATCH 3/4] Update PlayerManager.cs --- Robust.Client/Player/PlayerManager.cs | 479 +++++++++++++------------- 1 file changed, 239 insertions(+), 240 deletions(-) diff --git a/Robust.Client/Player/PlayerManager.cs b/Robust.Client/Player/PlayerManager.cs index e47d329e837..cdf83d4e0c6 100644 --- a/Robust.Client/Player/PlayerManager.cs +++ b/Robust.Client/Player/PlayerManager.cs @@ -10,312 +10,311 @@ using Robust.Shared.Player; using Robust.Shared.Utility; -namespace Robust.Client.Player +namespace Robust.Client.Player; + +/// +/// Here's the player controller. This will handle attaching GUIs and input to controllable things. +/// Why not just attach the inputs directly? It's messy! This makes the whole thing nicely encapsulated. +/// This class also communicates with the server to let the server control what entity it is attached to. +/// +internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager { + [Dependency] private readonly IClientNetManager _network = default!; + [Dependency] private readonly IBaseClient _client = default!; + /// - /// Here's the player controller. This will handle attaching GUIs and input to controllable things. - /// Why not just attach the inputs directly? It's messy! This makes the whole thing nicely encapsulated. - /// This class also communicates with the server to let the server control what entity it is attached to. + /// Received player states that had an unknown . /// - internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager - { - [Dependency] private readonly IClientNetManager _network = default!; - [Dependency] private readonly IBaseClient _client = default!; + private Dictionary _pendingStates = new(); + private List _pending = new(); - /// - /// Received player states that had an unknown . - /// - private Dictionary _pendingStates = new(); - private List _pending = new(); - - /// - public override ICommonSession[] NetworkedSessions + /// + public override ICommonSession[] NetworkedSessions + { + get { - get - { - return LocalSession != null - ? new[] { LocalSession } - : Array.Empty(); - } + return LocalSession != null + ? new[] { LocalSession } + : Array.Empty(); } + } - /// - public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1; + /// + public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1; - public LocalPlayer? LocalPlayer { get; private set; } + public LocalPlayer? LocalPlayer { get; private set; } - public event Action? LocalStatusChanged; - public event Action? PlayerListUpdated; - public event Action? LocalPlayerDetached; - public event Action? LocalPlayerAttached; - public event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged; + public event Action? LocalStatusChanged; + public event Action? PlayerListUpdated; + public event Action? LocalPlayerDetached; + public event Action? LocalPlayerAttached; + public event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged; - /// - public override void Initialize(int maxPlayers) + /// + public override void Initialize(int maxPlayers) + { + base.Initialize(maxPlayers); + _network.RegisterNetMessage(); + _network.RegisterNetMessage(HandlePlayerList); + PlayerStatusChanged += StatusChanged; + } + + private void StatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.Session == LocalPlayer?.Session) + LocalStatusChanged?.Invoke(e); + } + + public void SetupSinglePlayer(string name) + { + if (LocalSession != null) + throw new InvalidOperationException($"Player manager already running?"); + + var session = CreateAndAddSession(default, name); + session.ClientSide = true; + SetLocalSession(session); + Startup(); + PlayerListUpdated?.Invoke(); + } + + public void SetupMultiplayer(INetChannel channel) + { + if (LocalSession != null) + throw new InvalidOperationException($"Player manager already running?"); + + SetLocalSession(CreateAndAddSession(channel)); + Startup(); + _network.ClientSendMessage(new MsgPlayerListReq()); + } + + public void SetLocalSession(ICommonSession? session) + { + if (session == LocalSession) + return; + + var old = LocalSession; + + if (old?.AttachedEntity is { } oldUid) { - base.Initialize(maxPlayers); - _network.RegisterNetMessage(); - _network.RegisterNetMessage(HandlePlayerList); - PlayerStatusChanged += StatusChanged; + LocalSession = null; + LocalPlayer = null; + Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(oldUid)}."); + EntManager.EventBus.RaiseLocalEvent(oldUid, new LocalPlayerDetachedEvent(oldUid), true); + LocalPlayerDetached?.Invoke(oldUid); } - private void StatusChanged(object? sender, SessionStatusEventArgs e) + LocalSession = session; + LocalPlayer = session == null ? null : new LocalPlayer(session); + Sawmill.Info($"Changing local session from {old?.ToString() ?? "null"} to {session?.ToString() ?? "null"}."); + LocalSessionChanged?.Invoke((old, LocalSession)); + + if (session?.AttachedEntity is { } newUid) { - if (e.Session == LocalPlayer?.Session) - LocalStatusChanged?.Invoke(e); + Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(newUid)}."); + EntManager.EventBus.RaiseLocalEvent(newUid, new LocalPlayerAttachedEvent(newUid), true); + LocalPlayerAttached?.Invoke(newUid); } + } - public void SetupSinglePlayer(string name) - { - if (LocalSession != null) - throw new InvalidOperationException($"Player manager already running?"); + /// + public override void Shutdown() + { + SetAttachedEntity(LocalSession, null, out _); + LocalPlayer = null; + LocalSession = null; + _pendingStates.Clear(); + base.Shutdown(); + PlayerListUpdated?.Invoke(); + } - var session = CreateAndAddSession(default, name); - session.ClientSide = true; - SetLocalSession(session); - Startup(); - PlayerListUpdated?.Invoke(); - } + public override bool SetAttachedEntity(ICommonSession? session, EntityUid? uid, out ICommonSession? kicked, bool force = false) + { + kicked = null; + if (session == null) + return false; - public void SetupMultiplayer(INetChannel channel) - { - if (LocalSession != null) - throw new InvalidOperationException($"Player manager already running?"); + if (session.AttachedEntity == uid) + return true; + + var old = session.AttachedEntity; + if (!base.SetAttachedEntity(session, uid, out kicked, force)) + return false; + + if (session != LocalSession) + return true; - SetLocalSession(CreateAndAddSession(channel)); - Startup(); - _network.ClientSendMessage(new MsgPlayerListReq()); + if (old.HasValue) + { + Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(old)}."); + EntManager.EventBus.RaiseLocalEvent(old.Value, new LocalPlayerDetachedEvent(old.Value), true); + LocalPlayerDetached?.Invoke(old.Value); } - public void SetLocalSession(ICommonSession? session) + if (uid == null) { - if (session == LocalSession) - return; + Sawmill.Info($"Local player is no longer attached to any entity."); + return true; + } - var old = LocalSession; + if (!EntManager.EntityExists(uid)) + { + Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!"); + return true; + } - if (old?.AttachedEntity is { } oldUid) - { - LocalSession = null; - LocalPlayer = null; - Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(oldUid)}."); - EntManager.EventBus.RaiseLocalEvent(oldUid, new LocalPlayerDetachedEvent(oldUid), true); - LocalPlayerDetached?.Invoke(oldUid); - } + if (!EntManager.HasComponent(uid.Value)) + { + if (_client.RunLevel != ClientRunLevel.SinglePlayerGame) + Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues."); + var eye = Factory.GetComponent(); + eye.NetSyncEnabled = false; + EntManager.AddComponent(uid.Value, eye); + } - LocalSession = session; - LocalPlayer = session == null ? null : new LocalPlayer(session); - Sawmill.Info($"Changing local session from {old?.ToString() ?? "null"} to {session?.ToString() ?? "null"}."); - LocalSessionChanged?.Invoke((old, LocalSession)); + Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}."); + EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true); + LocalPlayerAttached?.Invoke(uid.Value); + return true; + } - if (session?.AttachedEntity is { } newUid) - { - Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(newUid)}."); - EntManager.EventBus.RaiseLocalEvent(newUid, new LocalPlayerAttachedEvent(newUid), true); - LocalPlayerAttached?.Invoke(newUid); - } - } + public void ApplyPlayerStates(IReadOnlyCollection list) + { + var dirty = ApplyStates(list, true); - /// - public override void Shutdown() + if (_pendingStates.Count == 0) { - SetAttachedEntity(LocalSession, null, out _); - LocalPlayer = null; - LocalSession = null; + // This is somewhat inefficient as it might try to re-apply states that failed just a moment ago. + _pending.Clear(); + _pending.AddRange(_pendingStates.Values); _pendingStates.Clear(); - base.Shutdown(); - PlayerListUpdated?.Invoke(); + dirty |= ApplyStates(_pending, false); } - public override bool SetAttachedEntity(ICommonSession? session, EntityUid? uid, out ICommonSession? kicked, bool force = false) - { - kicked = null; - if (session == null) - return false; + if (dirty) + PlayerListUpdated?.Invoke(); + } - if (session.AttachedEntity == uid) - return true; + private bool ApplyStates(IReadOnlyCollection list, bool fullList) + { + if (list.Count == 0) + return false; - var old = session.AttachedEntity; - if (!base.SetAttachedEntity(session, uid, out kicked, force)) - return false; + DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application. + , "Received player state without being connected?"); + DebugTools.Assert(LocalSession != null, "Received player state before Session finished setup."); - if (session != LocalSession) - return true; + var state = list.FirstOrDefault(s => s.UserId == LocalSession.UserId); - if (old.HasValue) + bool dirty = false; + if (state != null) + { + dirty = true; + if (!EntManager.TryGetEntity(state.ControlledEntity, out var uid) + && state.ControlledEntity is { Valid: true }) { - Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(old)}."); - EntManager.EventBus.RaiseLocalEvent(old.Value, new LocalPlayerDetachedEvent(old.Value), true); - LocalPlayerDetached?.Invoke(old.Value); + Sawmill.Error($"Received player state for local player with an unknown net entity!"); + _pendingStates[state.UserId] = state; } - - if (uid == null) + else { - Sawmill.Info($"Local player is no longer attached to any entity."); - return true; + _pendingStates.Remove(state.UserId); } - if (!EntManager.EntityExists(uid)) - { - Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!"); - return true; - } + SetAttachedEntity(LocalSession, uid, out _, true); + SetStatus(LocalSession, state.Status); + } - if (!EntManager.HasComponent(uid.Value)) - { - if (_client.RunLevel != ClientRunLevel.SinglePlayerGame) - Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues."); - var eye = Factory.GetComponent(); - eye.NetSyncEnabled = false; - EntManager.AddComponent(uid.Value, eye); - } + return UpdatePlayerList(list, fullList) || dirty; + } - Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}."); - EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true); - LocalPlayerAttached?.Invoke(uid.Value); - return true; - } + /// + /// Handles the incoming PlayerList message from the server. + /// + private void HandlePlayerList(MsgPlayerList msg) + { + ApplyPlayerStates(msg.Plyrs); + } - public void ApplyPlayerStates(IReadOnlyCollection list) + /// + /// Compares the server player list to the client one, and updates if needed. + /// + private bool UpdatePlayerList(IEnumerable remotePlayers, bool fullList) + { + var dirty = false; + var users = new List(); + foreach (var state in remotePlayers) { - var dirty = ApplyStates(list, true); + users.Add(state.UserId); - if (_pendingStates.Count == 0) + if (!EntManager.TryGetEntity(state.ControlledEntity, out var controlled) + && state.ControlledEntity is { Valid: true }) { - // This is somewhat inefficient as it might try to re-apply states that failed just a moment ago. - _pending.Clear(); - _pending.AddRange(_pendingStates.Values); - _pendingStates.Clear(); - dirty |= ApplyStates(_pending, false); + _pendingStates[state.UserId] = state; + } + else + { + _pendingStates.Remove(state.UserId); } - if (dirty) - PlayerListUpdated?.Invoke(); - } - - private bool ApplyStates(IReadOnlyCollection list, bool fullList) - { - if (list.Count == 0) - return false; - - DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application. - , "Received player state without being connected?"); - DebugTools.Assert(LocalSession != null, "Received player state before Session finished setup."); - - var state = list.FirstOrDefault(s => s.UserId == LocalSession.UserId); - - bool dirty = false; - if (state != null) + if (!InternalSessions.TryGetValue(state.UserId, out var session)) { + // This is a new userid, so we create a new session. + DebugTools.Assert(state.UserId != LocalPlayer?.UserId); + var newSession = (ICommonSessionInternal)CreateAndAddSession(state.UserId, state.Name); + SetStatus(newSession, state.Status); + SetAttachedEntity(newSession, controlled, out _, true); dirty = true; - if (!EntManager.TryGetEntity(state.ControlledEntity, out var uid) - && state.ControlledEntity is { Valid: true }) - { - Sawmill.Error($"Received player state for local player with an unknown net entity!"); - _pendingStates[state.UserId] = state; - } - else - { - _pendingStates.Remove(state.UserId); - } - - SetAttachedEntity(LocalSession, uid, out _, true); - SetStatus(LocalSession, state.Status); + continue; } - return UpdatePlayerList(list, fullList) || dirty; - } + // Check if the data is actually different + if (session.Name == state.Name + && session.Status == state.Status + && session.AttachedEntity == controlled) + { + continue; + } - /// - /// Handles the incoming PlayerList message from the server. - /// - private void HandlePlayerList(MsgPlayerList msg) - { - ApplyPlayerStates(msg.Plyrs); + dirty = true; + var local = (ICommonSessionInternal)session; + local.SetName(state.Name); + SetStatus(local, state.Status); + SetAttachedEntity(local, controlled, out _, true); } - /// - /// Compares the server player list to the client one, and updates if needed. - /// - private bool UpdatePlayerList(IEnumerable remotePlayers, bool fullList) + // Remove old users. This only works if the provided state is a list of all players + if (fullList) { - var dirty = false; - var users = new List(); - foreach (var state in remotePlayers) + foreach (var oldUser in InternalSessions.Keys.ToArray()) { - users.Add(state.UserId); - - if (!EntManager.TryGetEntity(state.ControlledEntity, out var controlled) - && state.ControlledEntity is { Valid: true }) - { - _pendingStates[state.UserId] = state; - } - else - { - _pendingStates.Remove(state.UserId); - } - - if (!InternalSessions.TryGetValue(state.UserId, out var session)) - { - // This is a new userid, so we create a new session. - DebugTools.Assert(state.UserId != LocalPlayer?.UserId); - var newSession = (ICommonSessionInternal)CreateAndAddSession(state.UserId, state.Name); - SetStatus(newSession, state.Status); - SetAttachedEntity(newSession, controlled, out _, true); - dirty = true; + if (users.Contains(oldUser)) continue; - } - // Check if the data is actually different - if (session.Name == state.Name - && session.Status == state.Status - && session.AttachedEntity == controlled) - { + if (InternalSessions[oldUser].ClientSide) continue; - } + DebugTools.Assert(oldUser != LocalUser + || LocalUser == null + || LocalUser == default(NetUserId), + "Client is still connected to the server but not in the list of players?"); + RemoveSession(oldUser); + _pendingStates.Remove(oldUser); dirty = true; - var local = (ICommonSessionInternal)session; - local.SetName(state.Name); - SetStatus(local, state.Status); - SetAttachedEntity(local, controlled, out _, true); } - - // Remove old users. This only works if the provided state is a list of all players - if (fullList) - { - foreach (var oldUser in InternalSessions.Keys.ToArray()) - { - if (users.Contains(oldUser)) - continue; - - if (InternalSessions[oldUser].ClientSide) - continue; - - DebugTools.Assert(oldUser != LocalUser - || LocalUser == null - || LocalUser == default(NetUserId), - "Client is still connected to the server but not in the list of players?"); - RemoveSession(oldUser); - _pendingStates.Remove(oldUser); - dirty = true; - } - } - - return dirty; } - public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) - { - if (LocalEntity == uid) - { - session = LocalSession!; - return true; - } + return dirty; + } - session = null; - return false; + public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) + { + if (LocalEntity == uid) + { + session = LocalSession!; + return true; } + + session = null; + return false; } } From 89d1819beb9a738b8ca173974488560085d73545 Mon Sep 17 00:00:00 2001 From: Ed <96445749+TheShuEd@users.noreply.github.com> Date: Tue, 12 May 2026 16:49:57 +0300 Subject: [PATCH 4/4] revert readonly --- Robust.Client/Player/PlayerManager.cs | 4 ++-- Robust.Server/Player/PlayerManager.cs | 12 ++++++------ Robust.Shared/Player/SharedPlayerManager.cs | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Robust.Client/Player/PlayerManager.cs b/Robust.Client/Player/PlayerManager.cs index cdf83d4e0c6..16d248ea5e6 100644 --- a/Robust.Client/Player/PlayerManager.cs +++ b/Robust.Client/Player/PlayerManager.cs @@ -19,8 +19,8 @@ namespace Robust.Client.Player; /// internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager { - [Dependency] private readonly IClientNetManager _network = default!; - [Dependency] private readonly IBaseClient _client = default!; + [Dependency] private IClientNetManager _network = default!; + [Dependency] private IBaseClient _client = default!; /// /// Received player states that had an unknown . diff --git a/Robust.Server/Player/PlayerManager.cs b/Robust.Server/Player/PlayerManager.cs index b82081ddb64..07323cfc950 100644 --- a/Robust.Server/Player/PlayerManager.cs +++ b/Robust.Server/Player/PlayerManager.cs @@ -27,12 +27,12 @@ internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManage 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!; + [Dependency] private IBaseServer _baseServer = default!; + [Dependency] private IGameTiming _timing = default!; + [Dependency] private IServerNetManager _network = default!; + [Dependency] private IReflectionManager _reflectionManager = default!; + [Dependency] private IEntityManager _entityManager = default!; + [Dependency] private IServerNetConfigurationManager _cfg = default!; public BoundKeyMap KeyMap { get; private set; } = default!; diff --git a/Robust.Shared/Player/SharedPlayerManager.cs b/Robust.Shared/Player/SharedPlayerManager.cs index 54ab6686fd9..2a78bb9e7dc 100644 --- a/Robust.Shared/Player/SharedPlayerManager.cs +++ b/Robust.Shared/Player/SharedPlayerManager.cs @@ -11,11 +11,11 @@ namespace Robust.Shared.Player; internal abstract partial class SharedPlayerManager : ISharedPlayerManager { - [Dependency] protected readonly IEntityManager EntManager = default!; - [Dependency] protected readonly IComponentFactory Factory = default!; - [Dependency] protected readonly ILogManager LogMan = default!; - [Dependency] protected readonly IGameTiming Timing = default!; - [Dependency] private readonly INetManager _netMan = default!; + [Dependency] protected IEntityManager EntManager = default!; + [Dependency] protected IComponentFactory Factory = default!; + [Dependency] protected ILogManager LogMan = default!; + [Dependency] protected IGameTiming Timing = default!; + [Dependency] private INetManager _netMan = default!; protected ISawmill Sawmill = default!;