From 40b21190f50a894ecd802077a14fe0f834f71df3 Mon Sep 17 00:00:00 2001 From: Anri Date: Sun, 2 Nov 2025 15:19:41 +0300 Subject: [PATCH 01/10] working-expample --- .../ServerNetConfigurationManager.cs | 104 +++++++++++++++--- .../Configuration/NetConfigurationManager.cs | 42 +++++++ 2 files changed, 133 insertions(+), 13 deletions(-) diff --git a/Robust.Server/Configuration/ServerNetConfigurationManager.cs b/Robust.Server/Configuration/ServerNetConfigurationManager.cs index 61e52181811..e8d9b328e38 100644 --- a/Robust.Server/Configuration/ServerNetConfigurationManager.cs +++ b/Robust.Server/Configuration/ServerNetConfigurationManager.cs @@ -6,6 +6,10 @@ using System.Collections.Generic; using Robust.Shared.IoC; using Robust.Shared.Replays; +using System; +using Robust.Shared.Player; +using Robust.Shared.Collections; +using Robust.Server.Player; namespace Robust.Server.Configuration; @@ -13,9 +17,12 @@ internal sealed class ServerNetConfigurationManager : NetConfigurationManager, I { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IReplayRecordingManager _replayRecording = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; private readonly Dictionary> _replicatedCVars = new(); + private readonly Dictionary> _replicatedInvoke = new(); + public override void SetupNetworking() { base.SetupNetworking(); @@ -106,31 +113,102 @@ protected override void ApplyNetVarChange( return; } - using var _ = Lock.ReadGuard(); - - foreach (var (name, value) in networkedVars) + var cVarChanges = new List(); + using (Lock.ReadGuard()) { - if (!_configVars.TryGetValue(name, out var cVar)) + foreach (var (name, value) in networkedVars) { - Sawmill.Warning($"{msgChannel} tried to replicate an unknown CVar '{name}.'"); - continue; + if (!_configVars.TryGetValue(name, out var cVar)) + { + Sawmill.Warning($"{msgChannel} tried to replicate an unknown CVar '{name}.'"); + continue; + } + + if (!cVar.Registered) + { + Sawmill.Warning($"{msgChannel} tried to replicate an unregistered CVar '{name}.'"); + continue; + } + + if ((cVar.Flags & CVar.REPLICATED) != 0) + { + clientCVars.TryGetValue(name, out var oldValue); + cVarChanges.Add(new(name, tick, value, oldValue ?? value)); + clientCVars[name] = value; + Sawmill.Debug($"name={name}, val={value}"); + } + else + { + Sawmill.Warning($"{msgChannel} tried to replicate an un-replicated CVar '{name}.'"); + } } + } + + foreach (var info in cVarChanges) + { + InvokeClientCvarChange(info, msgChannel); + } + } - if (!cVar.Registered) + private void InvokeClientCvarChange(CVarChangeInfo info, INetChannel msgChannel) + { + if (!_playerManager.TryGetSessionByChannel(msgChannel, out var session)) + { + Sawmill.Error($"Got client cvar change for NetChannel {msgChannel.UserId} without session!"); + return; + } + + if (!_replicatedInvoke.TryGetValue(info.Name, out var invokeList)) + return; + + foreach (var entry in invokeList.Entries) + { + try + { + entry.Value!.Invoke(info.NewValue, session, in info); + } + catch (Exception e) { - Sawmill.Warning($"{msgChannel} tried to replicate an unregistered CVar '{name}.'"); - continue; + Sawmill.Error($"Error while running InvokeClientCvarChange callback: {e}"); } + } + } - if ((cVar.Flags & CVar.REPLICATED) != 0) + /// + public override void OnClientCVarChanges(string name, Action onValueChanged) + { + base.OnClientCVarChanges(name, onValueChanged); + + using (Lock.WriteGuard()) + { + if (!_replicatedInvoke.TryGetValue(name, out var invoke)) { - clientCVars[name] = value; - Sawmill.Debug($"name={name}, val={value}"); + InvokeList invokeList = new(); + invokeList.AddInPlace((object value, ICommonSession session, in CVarChangeInfo _) => onValueChanged((T)value, session), onValueChanged); + + _replicatedInvoke.Add(name, invokeList); } else { - Sawmill.Warning($"{msgChannel} tried to replicate an un-replicated CVar '{name}.'"); + invoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo _) => onValueChanged((T)value, session),onValueChanged); + } + } + } + + /// + public override void UnsubClientCVarChanges(string name, Action onValueChanged) + { + base.UnsubClientCVarChanges(name, onValueChanged); + + using (Lock.WriteGuard()) + { + if (!_replicatedInvoke.TryGetValue(name, out var invoke)) + { + Sawmill.Error($"Trying to unsubscribe for cvar {name} changes that dont have any subscriptions at all!"); + return; } + + invoke.RemoveInPlace(onValueChanged); } } } diff --git a/Robust.Shared/Configuration/NetConfigurationManager.cs b/Robust.Shared/Configuration/NetConfigurationManager.cs index e4c73db6243..d61aeeb9e9c 100644 --- a/Robust.Shared/Configuration/NetConfigurationManager.cs +++ b/Robust.Shared/Configuration/NetConfigurationManager.cs @@ -5,6 +5,7 @@ using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Network.Messages; +using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -57,6 +58,41 @@ public interface INetConfigurationManager : IConfigurationManager /// Replicated CVar of the client. T GetClientCVar(INetChannel channel, CVarDef definition) where T : notnull => GetClientCVar(channel, definition.Name); + + /// + /// Listen for an event for if the config value changes on client changes. Does nothing on client side + /// + /// CVar type. + /// Name of the CVar. + /// The delegate to run when the cvar was changed. + void OnClientCVarChanges(string name, Action onChanged) where T : notnull; + + /// + /// Listen for an event for if the config value changes on client changes. Does nothing on client side + /// + /// CVar type. + /// The CVar. + /// The delegate to run when the cvar was changed. + void OnClientCVarChanges(CVarDef definition, Action onChanged) where T : notnull + => OnClientCVarChanges(definition.Name, onChanged); + + /// + /// Unsubscribe an event previously registered with . + /// + /// CVar type. + /// Name of the CVar. + /// The delegate to run when the cvar was changed. + void UnsubClientCVarChanges(string name, Action onChanged) where T : notnull; + + /// + /// Unsubscribe an event previously registered with . + /// + /// CVar type. + /// The CVar. + /// The delegate to run when the cvar was changed. + void UnsubClientCVarChanges(CVarDef definition, Action onChanged) where T : notnull + => UnsubClientCVarChanges(definition.Name, onChanged); + } internal interface INetConfigurationManagerInternal : INetConfigurationManager, IConfigurationManagerInternal @@ -64,6 +100,8 @@ internal interface INetConfigurationManagerInternal : INetConfigurationManager, } + public delegate void ClientValueChangedDelegate(object value, ICommonSession session, in CVarChangeInfo info); + /// internal abstract class NetConfigurationManager : ConfigurationManager, INetConfigurationManagerInternal { @@ -197,5 +235,9 @@ public void SyncConnectingClient(INetChannel client) /// public abstract T GetClientCVar(INetChannel channel, string name); + + public virtual void OnClientCVarChanges(string name, Action onChanged) where T : notnull { } + + public virtual void UnsubClientCVarChanges(string name, Action onChanged) where T : notnull { } } } From bd371ad8e73e8f261cfc5cd12db5c3cb9f032c29 Mon Sep 17 00:00:00 2001 From: Anri Date: Mon, 3 Nov 2025 19:48:52 +0300 Subject: [PATCH 02/10] smooth-shared-usage-implementation --- .../ClientNetConfigurationManager.cs | 26 +++++++++++++++++++ .../ServerNetConfigurationManager.cs | 4 --- .../Configuration/NetConfigurationManager.cs | 4 +-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Robust.Client/Configuration/ClientNetConfigurationManager.cs b/Robust.Client/Configuration/ClientNetConfigurationManager.cs index 8722cb1049a..2d60b9d5130 100644 --- a/Robust.Client/Configuration/ClientNetConfigurationManager.cs +++ b/Robust.Client/Configuration/ClientNetConfigurationManager.cs @@ -7,6 +7,8 @@ using Robust.Shared.Network; using Robust.Shared.Replays; using Robust.Shared.Utility; +using Robust.Shared.Player; +using Robust.Client.Player; namespace Robust.Client.Configuration; @@ -15,6 +17,7 @@ internal sealed class ClientNetConfigurationManager : NetConfigurationManager, I [Dependency] private readonly IBaseClient _client = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IReplayRecordingManager _replay = default!; + [Dependency] private readonly IPlayerManager _player = default!; private bool _receivedInitialNwVars = false; @@ -136,4 +139,27 @@ private void ApplyClientNetVarChange(List<(string name, object value)> networked /// public override T GetClientCVar(INetChannel channel, string name) => GetCVar(name); + + public override void OnClientCVarChanges(string name, Action onChanged) + { + if (_player.LocalSession is not { } localSession) + { + Sawmill.Error("Got null local session for client!"); + return; + } + + OnValueChanged(name, (x) => onChanged(x, localSession), true); + } + + public override void UnsubClientCVarChanges(string name, Action onChanged) + { + if (_player.LocalSession is not { } localSession) + { + Sawmill.Error("Got null local session for client!"); + return; + } + + UnsubValueChanged(name, (x) => onChanged(x, localSession)); + } + } diff --git a/Robust.Server/Configuration/ServerNetConfigurationManager.cs b/Robust.Server/Configuration/ServerNetConfigurationManager.cs index e8d9b328e38..2667045600a 100644 --- a/Robust.Server/Configuration/ServerNetConfigurationManager.cs +++ b/Robust.Server/Configuration/ServerNetConfigurationManager.cs @@ -177,8 +177,6 @@ private void InvokeClientCvarChange(CVarChangeInfo info, INetChannel msgChannel) /// public override void OnClientCVarChanges(string name, Action onValueChanged) { - base.OnClientCVarChanges(name, onValueChanged); - using (Lock.WriteGuard()) { if (!_replicatedInvoke.TryGetValue(name, out var invoke)) @@ -198,8 +196,6 @@ public override void OnClientCVarChanges(string name, Action public override void UnsubClientCVarChanges(string name, Action onValueChanged) { - base.UnsubClientCVarChanges(name, onValueChanged); - using (Lock.WriteGuard()) { if (!_replicatedInvoke.TryGetValue(name, out var invoke)) diff --git a/Robust.Shared/Configuration/NetConfigurationManager.cs b/Robust.Shared/Configuration/NetConfigurationManager.cs index d61aeeb9e9c..420bab0374d 100644 --- a/Robust.Shared/Configuration/NetConfigurationManager.cs +++ b/Robust.Shared/Configuration/NetConfigurationManager.cs @@ -236,8 +236,8 @@ public void SyncConnectingClient(INetChannel client) /// public abstract T GetClientCVar(INetChannel channel, string name); - public virtual void OnClientCVarChanges(string name, Action onChanged) where T : notnull { } + public abstract void OnClientCVarChanges(string name, Action onChanged) where T : notnull; - public virtual void UnsubClientCVarChanges(string name, Action onChanged) where T : notnull { } + public abstract void UnsubClientCVarChanges(string name, Action onChanged) where T : notnull; } } From 73306825ae579381f0760b05dcbb6089a2efbc9b Mon Sep 17 00:00:00 2001 From: Anri Date: Wed, 5 Nov 2025 16:32:27 +0300 Subject: [PATCH 03/10] update-method-for-handling-disconnect --- .../ClientNetConfigurationManager.cs | 4 +- .../ServerNetConfigurationManager.cs | 45 ++++++++++++++----- .../Configuration/NetConfigurationManager.cs | 16 +++---- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/Robust.Client/Configuration/ClientNetConfigurationManager.cs b/Robust.Client/Configuration/ClientNetConfigurationManager.cs index 2d60b9d5130..b7781286a01 100644 --- a/Robust.Client/Configuration/ClientNetConfigurationManager.cs +++ b/Robust.Client/Configuration/ClientNetConfigurationManager.cs @@ -140,7 +140,7 @@ private void ApplyClientNetVarChange(List<(string name, object value)> networked /// public override T GetClientCVar(INetChannel channel, string name) => GetCVar(name); - public override void OnClientCVarChanges(string name, Action onChanged) + public override void OnClientCVarChanges(string name, Action onChanged, Action? onDisconnect) { if (_player.LocalSession is not { } localSession) { @@ -151,7 +151,7 @@ public override void OnClientCVarChanges(string name, Action(name, (x) => onChanged(x, localSession), true); } - public override void UnsubClientCVarChanges(string name, Action onChanged) + public override void UnsubClientCVarChanges(string name, Action onChanged, Action? onDisconnect) { if (_player.LocalSession is not { } localSession) { diff --git a/Robust.Server/Configuration/ServerNetConfigurationManager.cs b/Robust.Server/Configuration/ServerNetConfigurationManager.cs index 2667045600a..28a5e32fc2f 100644 --- a/Robust.Server/Configuration/ServerNetConfigurationManager.cs +++ b/Robust.Server/Configuration/ServerNetConfigurationManager.cs @@ -1,15 +1,16 @@ +using System; +using System.Collections.Generic; +using Robust.Server.Player; +using Robust.Shared.Collections; using Robust.Shared.Configuration; +using Robust.Shared.Enums; +using Robust.Shared.IoC; using Robust.Shared.Network; using Robust.Shared.Network.Messages; +using Robust.Shared.Player; +using Robust.Shared.Replays; using Robust.Shared.Timing; using Robust.Shared.Utility; -using System.Collections.Generic; -using Robust.Shared.IoC; -using Robust.Shared.Replays; -using System; -using Robust.Shared.Player; -using Robust.Shared.Collections; -using Robust.Server.Player; namespace Robust.Server.Configuration; @@ -175,7 +176,7 @@ private void InvokeClientCvarChange(CVarChangeInfo info, INetChannel msgChannel) } /// - public override void OnClientCVarChanges(string name, Action onValueChanged) + public override void OnClientCVarChanges(string name, Action onValueChanged, Action? onDisconnect) { using (Lock.WriteGuard()) { @@ -188,14 +189,29 @@ public override void OnClientCVarChanges(string name, Action onValueChanged((T)value, session),onValueChanged); + invoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo _) => onValueChanged((T)value, session), onValueChanged); } } + + if (onDisconnect is null) + return; + + _playerManager.PlayerStatusChanged += (_, args) => + { + if (args.NewStatus == SessionStatus.Disconnected) + onDisconnect?.Invoke(args.Session); + }; } /// - public override void UnsubClientCVarChanges(string name, Action onValueChanged) + public override void UnsubClientCVarChanges(string name, Action onValueChanged, Action? onDisconnect) { + _playerManager.PlayerStatusChanged -= (_, args) => + { + if (args.NewStatus == SessionStatus.Disconnected) + onDisconnect?.Invoke(args.Session); + }; + using (Lock.WriteGuard()) { if (!_replicatedInvoke.TryGetValue(name, out var invoke)) @@ -206,5 +222,14 @@ public override void UnsubClientCVarChanges(string name, Action + { + if (args.NewStatus == SessionStatus.Disconnected) + onDisconnect?.Invoke(args.Session); + }; } } diff --git a/Robust.Shared/Configuration/NetConfigurationManager.cs b/Robust.Shared/Configuration/NetConfigurationManager.cs index 420bab0374d..ca7ac8fcfcd 100644 --- a/Robust.Shared/Configuration/NetConfigurationManager.cs +++ b/Robust.Shared/Configuration/NetConfigurationManager.cs @@ -65,7 +65,7 @@ T GetClientCVar(INetChannel channel, CVarDef definition) where T : notnull /// CVar type. /// Name of the CVar. /// The delegate to run when the cvar was changed. - void OnClientCVarChanges(string name, Action onChanged) where T : notnull; + void OnClientCVarChanges(string name, Action onChanged, Action? onDisconnect) where T : notnull; /// /// Listen for an event for if the config value changes on client changes. Does nothing on client side @@ -73,8 +73,8 @@ T GetClientCVar(INetChannel channel, CVarDef definition) where T : notnull /// CVar type. /// The CVar. /// The delegate to run when the cvar was changed. - void OnClientCVarChanges(CVarDef definition, Action onChanged) where T : notnull - => OnClientCVarChanges(definition.Name, onChanged); + void OnClientCVarChanges(CVarDef definition, Action onChanged, Action? onDisconnect) where T : notnull + => OnClientCVarChanges(definition.Name, onChanged, onDisconnect); /// /// Unsubscribe an event previously registered with . @@ -82,7 +82,7 @@ void OnClientCVarChanges(CVarDef definition, Action onC /// CVar type. /// Name of the CVar. /// The delegate to run when the cvar was changed. - void UnsubClientCVarChanges(string name, Action onChanged) where T : notnull; + void UnsubClientCVarChanges(string name, Action onChanged, Action? onDisconnect) where T : notnull; /// /// Unsubscribe an event previously registered with . @@ -90,8 +90,8 @@ void OnClientCVarChanges(CVarDef definition, Action onC /// CVar type. /// The CVar. /// The delegate to run when the cvar was changed. - void UnsubClientCVarChanges(CVarDef definition, Action onChanged) where T : notnull - => UnsubClientCVarChanges(definition.Name, onChanged); + void UnsubClientCVarChanges(CVarDef definition, Action onChanged, Action? onDisconnect) where T : notnull + => UnsubClientCVarChanges(definition.Name, onChanged, onDisconnect); } @@ -236,8 +236,8 @@ public void SyncConnectingClient(INetChannel client) /// public abstract T GetClientCVar(INetChannel channel, string name); - public abstract void OnClientCVarChanges(string name, Action onChanged) where T : notnull; + public abstract void OnClientCVarChanges(string name, Action onChanged, Action? onDisconnect) where T : notnull; - public abstract void UnsubClientCVarChanges(string name, Action onChanged) where T : notnull; + public abstract void UnsubClientCVarChanges(string name, Action onChanged, Action? onDisconnect) where T : notnull; } } From 496389354f1dafa422bac85059f080e6cadf3130 Mon Sep 17 00:00:00 2001 From: Anri Date: Wed, 5 Nov 2025 23:12:19 +0300 Subject: [PATCH 04/10] working-disconnect-sub-and-tests --- .../ServerNetConfigurationManager.cs | 87 +++-- .../NetConfigurationManagerTest.cs | 352 ++++++++++++++++++ 2 files changed, 406 insertions(+), 33 deletions(-) create mode 100644 Robust.UnitTesting/Shared/Configuration/NetConfigurationManagerTest.cs diff --git a/Robust.Server/Configuration/ServerNetConfigurationManager.cs b/Robust.Server/Configuration/ServerNetConfigurationManager.cs index 28a5e32fc2f..74b9bd3570a 100644 --- a/Robust.Server/Configuration/ServerNetConfigurationManager.cs +++ b/Robust.Server/Configuration/ServerNetConfigurationManager.cs @@ -22,19 +22,25 @@ internal sealed class ServerNetConfigurationManager : NetConfigurationManager, I private readonly Dictionary> _replicatedCVars = new(); - private readonly Dictionary> _replicatedInvoke = new(); + private readonly Dictionary _replicatedInvokes = new(); + public override void SetupNetworking() { base.SetupNetworking(); NetManager.Connected += PeerConnected; NetManager.Disconnect += PeerDisconnected; + _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; } public override void Shutdown() { base.Shutdown(); + + _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged; + _replicatedCVars.Clear(); + _replicatedInvokes.Clear(); } private void PeerConnected(object? sender, NetChannelArgs e) @@ -47,6 +53,27 @@ private void PeerDisconnected(object? sender, NetDisconnectedArgs e) _replicatedCVars.Remove(e.Channel); } + private void OnPlayerStatusChanged(object? _, SessionStatusEventArgs args) + { + if (args.NewStatus != SessionStatus.Disconnected) + return; + + foreach (var (_, cVarInvoke) in _replicatedInvokes) + { + foreach (var entry in cVarInvoke.DisconnectDelegate.Entries) + { + try + { + entry.Value!.Invoke(args.Session); + } + catch (Exception e) + { + Sawmill.Error($"Error while running {nameof(DisconnectDelegate)} for replicated CVars callback: {e}"); + } + } + } + } + /// public override T GetClientCVar(INetChannel channel, string name) { @@ -159,10 +186,10 @@ private void InvokeClientCvarChange(CVarChangeInfo info, INetChannel msgChannel) return; } - if (!_replicatedInvoke.TryGetValue(info.Name, out var invokeList)) + if (!_replicatedInvokes.TryGetValue(info.Name, out var cVarInvokes)) return; - foreach (var entry in invokeList.Entries) + foreach (var entry in cVarInvokes.ClientChangeInvoke.Entries) { try { @@ -170,7 +197,7 @@ private void InvokeClientCvarChange(CVarChangeInfo info, INetChannel msgChannel) } catch (Exception e) { - Sawmill.Error($"Error while running InvokeClientCvarChange callback: {e}"); + Sawmill.Error($"Error while running {nameof(ClientValueChangedDelegate)} for replicated CVars callback: {e}"); } } } @@ -180,56 +207,50 @@ public override void OnClientCVarChanges(string name, Action invokeList = new(); - invokeList.AddInPlace((object value, ICommonSession session, in CVarChangeInfo _) => onValueChanged((T)value, session), onValueChanged); + cVarInvokes = new ReplicatedCVarInvokes { }; + cVarInvokes.ClientChangeInvoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo _) => onValueChanged((T)value, session), onValueChanged); - _replicatedInvoke.Add(name, invokeList); + _replicatedInvokes.Add(name, cVarInvokes); } else { - invoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo _) => onValueChanged((T)value, session), onValueChanged); + cVarInvokes.ClientChangeInvoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo _) => onValueChanged((T)value, session), onValueChanged); } - } - if (onDisconnect is null) - return; + if (onDisconnect is null) + return; - _playerManager.PlayerStatusChanged += (_, args) => - { - if (args.NewStatus == SessionStatus.Disconnected) - onDisconnect?.Invoke(args.Session); - }; + cVarInvokes.DisconnectDelegate.AddInPlace(session => onDisconnect(session), onDisconnect); + } } /// public override void UnsubClientCVarChanges(string name, Action onValueChanged, Action? onDisconnect) { - _playerManager.PlayerStatusChanged -= (_, args) => - { - if (args.NewStatus == SessionStatus.Disconnected) - onDisconnect?.Invoke(args.Session); - }; - using (Lock.WriteGuard()) { - if (!_replicatedInvoke.TryGetValue(name, out var invoke)) + if (!_replicatedInvokes.TryGetValue(name, out var cVarInvokes)) { - Sawmill.Error($"Trying to unsubscribe for cvar {name} changes that dont have any subscriptions at all!"); + Sawmill.Warning($"Trying to unsubscribe for cvar {name} changes that dont have any subscriptions at all!"); return; } - invoke.RemoveInPlace(onValueChanged); + cVarInvokes.ClientChangeInvoke.RemoveInPlace(onValueChanged); + + if (onDisconnect is null) + return; + + cVarInvokes.DisconnectDelegate.RemoveInPlace(onDisconnect); } + } - if (onDisconnect is null) - return; + private delegate void DisconnectDelegate(ICommonSession session); - _playerManager.PlayerStatusChanged += (_, args) => - { - if (args.NewStatus == SessionStatus.Disconnected) - onDisconnect?.Invoke(args.Session); - }; + private sealed class ReplicatedCVarInvokes + { + public InvokeList ClientChangeInvoke = new(); + public InvokeList DisconnectDelegate = new(); } } diff --git a/Robust.UnitTesting/Shared/Configuration/NetConfigurationManagerTest.cs b/Robust.UnitTesting/Shared/Configuration/NetConfigurationManagerTest.cs new file mode 100644 index 00000000000..e3b216c5c32 --- /dev/null +++ b/Robust.UnitTesting/Shared/Configuration/NetConfigurationManagerTest.cs @@ -0,0 +1,352 @@ +using NUnit.Framework; +using Robust.Shared.Configuration; +using Robust.Shared.Network; +using Robust.Shared.Player; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Robust.UnitTesting.Shared.Configuration; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +[TestOf(typeof(NetConfigurationManager))] +internal sealed class NetConfigurationManagerTest : RobustIntegrationTest +{ + [Test] + public async Task TestSubscribeUnsubscribe() + { + using var server = StartServer(); + using var client = StartClient(); + + await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + + var serverNetConfiguration = server.ResolveDependency(); + var clientNetConfiguration = client.ResolveDependency(); + + // CVar def consts + const string CVarName = "net.foo_bar"; + const CVar CVarFlags = CVar.CLIENT | CVar.REPLICATED; + const int DefaultValue = 1; + + // setup debug CVar + server.Post(() => + { + serverNetConfiguration.RegisterCVar(CVarName, DefaultValue, CVarFlags); + }); + + client.Post(() => + { + clientNetConfiguration.RegisterCVar(CVarName, DefaultValue, CVarFlags); + }); + + await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + // connect client + Assert.DoesNotThrow(() => client.SetConnectTarget(server)); + await client.WaitPost(() => + { + client.Resolve().ClientConnect(null!, 0, null!); + }); + + await RunTicks(server, client); + + var session = server.PlayerMan.Sessions.First(); + + Assert.Multiple(() => + { + Assert.That(serverNetConfiguration.GetClientCVar(session.Channel, CVarName), Is.EqualTo(DefaultValue)); + Assert.That(clientNetConfiguration.GetClientCVar(session.Channel, CVarName), Is.EqualTo(DefaultValue)); + }); + + + ICommonSession? subscribeSession = default!; + var SubscribeValue = 0; + var timesRan = 0; + void ClientValueChanged(int value, ICommonSession session) + { + timesRan++; + SubscribeValue = value; + subscribeSession = session; + } + + // actually subscribe + server.Post(() => + { + serverNetConfiguration.OnClientCVarChanges(CVarName, ClientValueChanged, null); + }); + + // set new value in client + const int NewValue = 8; + Assert.That(NewValue, Is.Not.EqualTo(DefaultValue)); + client.Post(() => + { + clientNetConfiguration.SetCVar(CVarName, NewValue); + }); + + await RunTicks(server, client); + + // assert handling cvar change and receiving event + Assert.Multiple(() => + { + Assert.That(clientNetConfiguration.GetClientCVar(session.Channel, CVarName), Is.EqualTo(NewValue)); + Assert.That(serverNetConfiguration.GetClientCVar(session.Channel, CVarName), Is.EqualTo(NewValue)); + + Assert.That(timesRan, Is.EqualTo(1)); + Assert.That(SubscribeValue, Is.EqualTo(NewValue)); + Assert.That(subscribeSession, Is.EqualTo(session)); + }); + + // unsubscribe + server.Post(() => + { + serverNetConfiguration.UnsubClientCVarChanges(CVarName, ClientValueChanged, null); + }); + + // set new value in client + const int UnsubValue = 16; + Assert.That(UnsubValue, Is.Not.EqualTo(NewValue)); + client.Post(() => + { + clientNetConfiguration.SetCVar(CVarName, UnsubValue); + }); + + await RunTicks(server, client); + + // assert handling cvar change and unsubscribing + Assert.Multiple(() => + { + Assert.That(clientNetConfiguration.GetClientCVar(session.Channel, CVarName), Is.EqualTo(UnsubValue)); + Assert.That(serverNetConfiguration.GetClientCVar(session.Channel, CVarName), Is.EqualTo(UnsubValue)); + + Assert.That(timesRan, Is.EqualTo(1)); + Assert.That(SubscribeValue, Is.EqualTo(NewValue)); + }); + + // now check how disconnect subscribe works + ICommonSession? disconnectSession = null; + var disconnectTimesRun = 0; + void OnDisconnect(ICommonSession session) + { + disconnectSession = session; + disconnectTimesRun++; + } + + server.Post(() => + { + serverNetConfiguration.OnClientCVarChanges(CVarName, ClientValueChanged, OnDisconnect); + }); + + // change value in client + client.Post(() => + { + clientNetConfiguration.SetCVar(CVarName, DefaultValue); + }); + + await RunTicks(server, client); + + // disconnect event don't fire on changing CVar + Assert.Multiple(() => + { + Assert.That(disconnectTimesRun, Is.EqualTo(0)); + Assert.That(disconnectSession, Is.EqualTo(null)); + }); + + // disconnect + await client.WaitPost(() => client.Resolve().ClientDisconnect("")); + + await RunTicks(server, client); + + Assert.Multiple(() => + { + Assert.That(disconnectTimesRun, Is.EqualTo(1)); + Assert.That(disconnectSession, Is.EqualTo(session)); + }); + + // reset for proper handling assertions and prevent colliding with new session + disconnectSession = null; + // again connect client + Assert.DoesNotThrow(() => client.SetConnectTarget(server)); + await client.WaitPost(() => + { + client.Resolve().ClientConnect(null!, 0, null!); + }); + + await RunTicks(server, client); + session = server.PlayerMan.Sessions.First(); + + // also check if somehow disconnectSession not null and have a strange session + Assert.Multiple(() => + { + Assert.That(disconnectTimesRun, Is.EqualTo(1)); + Assert.That(disconnectSession, Is.EqualTo(null)); + Assert.That(disconnectSession, Is.Not.EqualTo(session)); + }); + + // now unsubscribe + server.Post(() => + { + serverNetConfiguration.UnsubClientCVarChanges(CVarName, ClientValueChanged, OnDisconnect); + }); + + await RunTicks(server, client); + + // for current logic this shouldn't fire disconnect event. + Assert.Multiple(() => + { + Assert.That(disconnectTimesRun, Is.EqualTo(1)); + Assert.That(disconnectSession, Is.EqualTo(null)); + Assert.That(disconnectSession, Is.Not.EqualTo(session)); + }); + + // disconnect + await client.WaitPost(() => client.Resolve().ClientDisconnect("")); + + await RunTicks(server, client); + + // assert that unsubscribed event wasn't fired + Assert.Multiple(() => + { + Assert.That(disconnectTimesRun, Is.EqualTo(1)); + Assert.That(disconnectSession, Is.EqualTo(null)); + Assert.That(disconnectSession, Is.Not.EqualTo(session)); + }); + + } + + [Test] + public async Task TestSubscribeUnsubscribeMultipleClients() + { + const int ClientAmount = 4; + using var server = StartServer(); + ClientIntegrationInstance[] clients = new ClientIntegrationInstance[ClientAmount]; + + // CVar def consts + const string CVarName = "net.foo_bar"; + const CVar CVarFlags = CVar.CLIENT | CVar.REPLICATED; + const int DefaultValue = -1; + + HashSet eventSessions = []; + Dictionary clientValues = []; + void ClientValueChanged(int value, ICommonSession session) + { + eventSessions.Add(session); + clientValues[session] = value; + } + + HashSet eventDisconnectedSessions = []; + void OnDisconnect(ICommonSession session) + { + eventDisconnectedSessions.Add(session); + } + + // setup debug CVar + server.Post(() => + { + server.Resolve().RegisterCVar(CVarName, DefaultValue, CVarFlags); + server.Resolve().OnClientCVarChanges(CVarName, ClientValueChanged, OnDisconnect); + + }); + + // for this collection I need order of adding + List clientSessions = new(); + for (int i = 0; i < ClientAmount; i++) + { + var client = StartClient(); + clients[i] = client; + + client.Post(() => + { + client.Resolve().RegisterCVar(CVarName, DefaultValue, CVarFlags); + }); + + await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + + Assert.DoesNotThrow(() => client.SetConnectTarget(server)); + await client.WaitPost(() => + { + client.Resolve().ClientConnect(null!, 0, null!); + }); + + await RunTicks(server, client); + + clientSessions.Add(server.PlayerMan.Sessions.Except(clientSessions).First()); + } + + // check that we got correct sessions + Assert.That(clientSessions.Count, Is.EqualTo(ClientAmount)); + + // server invoke subscribed events of replicated CVar on client connect + Assert.Multiple(() => + { + Assert.That(eventSessions.Count, Is.EqualTo(ClientAmount)); + Assert.That(clientValues.Count, Is.EqualTo(ClientAmount)); + + Assert.That(clientValues.Values.Distinct().Count, Is.EqualTo(1)); + Assert.That(clientValues.Values.Distinct().First(), Is.EqualTo(DefaultValue)); + }); + + eventSessions.Clear(); + clientValues.Clear(); + + // try to change CVar on every client EXCEPT last one + for (int i = 0; i < ClientAmount - 1; i++) + { + var client = clients[i]; + // set new value in client + Assert.That(i, Is.Not.EqualTo(DefaultValue)); + client.Post(() => + { + client.Resolve().SetCVar(CVarName, i); + }); + + await RunTicks(server, client); + } + + Assert.Multiple(() => + { + // session events worked correctly (reminder: last one haven't changed it CVar) + Assert.That(eventSessions.Count, Is.EqualTo(ClientAmount - 1)); + Assert.That(eventDisconnectedSessions.Count, Is.EqualTo(0)); + + for (int i = 0; i < ClientAmount - 1; i++) + { + var currentSession = clientSessions[i]; + + int? value = null; + Assert.DoesNotThrow(() => value = clientValues[currentSession]); + + // check if session wasn't messed up + Assert.That(value, Is.EqualTo(i)); + } + + var lastSession = clientSessions[ClientAmount - 1]; + Assert.That(clientValues.ContainsKey(lastSession), Is.EqualTo(false)); + }); + + for (int i = 0; i < ClientAmount; i++) + { + var client = clients[i]; + + await client.WaitPost(() => client.Resolve().ClientDisconnect("")); + + await RunTicks(server, client); + + // assert that every disconnect result in event raising + Assert.That(eventDisconnectedSessions.Count, Is.EqualTo(i + 1)); + } + + // we received same sessions EXCEPT last one + Assert.That(eventDisconnectedSessions.Except(eventSessions), Is.EqualTo(new HashSet { clientSessions[ClientAmount - 1] })); + Assert.That(clientSessions, Is.EqualTo(eventDisconnectedSessions)); + } + + private async Task RunTicks(IntegrationInstance server, IntegrationInstance client, int numberOfTicks = 5) + { + for (int i = 0; i < numberOfTicks; i++) + { + await server.WaitRunTicks(1); + await client.WaitRunTicks(1); + } + } +} + From a74467f2899c9feb90b58c54f021e8b99d0e2599 Mon Sep 17 00:00:00 2001 From: Anri Date: Thu, 6 Nov 2025 10:54:22 +0300 Subject: [PATCH 05/10] fix-tests --- .../Shared/Configuration/ConfigurationManagerTest.cs | 2 ++ Robust.UnitTesting/Shared/GameObjects/EntityState_Tests.cs | 2 ++ .../Shared/GameObjects/EntitySystemManagerOrderTest.cs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/Robust.UnitTesting/Shared/Configuration/ConfigurationManagerTest.cs b/Robust.UnitTesting/Shared/Configuration/ConfigurationManagerTest.cs index c8f1d12d44d..ffdc1781825 100644 --- a/Robust.UnitTesting/Shared/Configuration/ConfigurationManagerTest.cs +++ b/Robust.UnitTesting/Shared/Configuration/ConfigurationManagerTest.cs @@ -1,6 +1,7 @@ using Moq; using NUnit.Framework; using Robust.Server.Configuration; +using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.IoC; using Robust.Shared.Log; @@ -142,6 +143,7 @@ private IConfigurationManager MakeCfg() var collection = new DependencyCollection(); collection.RegisterInstance(new Mock().Object); collection.RegisterInstance(new Mock().Object); + collection.RegisterInstance(new Mock().Object); collection.Register(); collection.Register(); collection.Register(); diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityState_Tests.cs b/Robust.UnitTesting/Shared/GameObjects/EntityState_Tests.cs index e042a1aa59a..3686eb0eab9 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityState_Tests.cs +++ b/Robust.UnitTesting/Shared/GameObjects/EntityState_Tests.cs @@ -3,6 +3,7 @@ using Moq; using NUnit.Framework; using Robust.Server.Configuration; +using Robust.Server.Player; using Robust.Server.Reflection; using Robust.Server.Serialization; using Robust.Shared.Configuration; @@ -45,6 +46,7 @@ public void ComponentChangedSerialized() container.Register(); container.Register(); container.RegisterInstance(new Mock().Object); + container.RegisterInstance(new Mock().Object); container.BuildGraph(); var cfg = container.Resolve(); diff --git a/Robust.UnitTesting/Shared/GameObjects/EntitySystemManagerOrderTest.cs b/Robust.UnitTesting/Shared/GameObjects/EntitySystemManagerOrderTest.cs index 70cee4e8746..ef2a99a7a7b 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntitySystemManagerOrderTest.cs +++ b/Robust.UnitTesting/Shared/GameObjects/EntitySystemManagerOrderTest.cs @@ -4,6 +4,7 @@ using Moq; using NUnit.Framework; using Robust.Server.Configuration; +using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.Exceptions; @@ -89,6 +90,7 @@ public void Test() deps.RegisterInstance(new Mock().Object); deps.Register(); deps.RegisterInstance(new Mock().Object); + deps.RegisterInstance(new Mock().Object); // WHEN WILL THE SUFFERING END deps.RegisterInstance(new Mock().Object); From e948a5c8a60469116d1287df438be13d40232c3f Mon Sep 17 00:00:00 2001 From: Anri Date: Thu, 4 Dec 2025 22:46:08 +0300 Subject: [PATCH 06/10] add-client-flags-checks --- .../ServerNetConfigurationManager.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Robust.Server/Configuration/ServerNetConfigurationManager.cs b/Robust.Server/Configuration/ServerNetConfigurationManager.cs index 74b9bd3570a..2dd21717595 100644 --- a/Robust.Server/Configuration/ServerNetConfigurationManager.cs +++ b/Robust.Server/Configuration/ServerNetConfigurationManager.cs @@ -205,6 +205,18 @@ private void InvokeClientCvarChange(CVarChangeInfo info, INetChannel msgChannel) /// public override void OnClientCVarChanges(string name, Action onValueChanged, Action? onDisconnect) { + if (!_configVars.TryGetValue(name, out var cVar)) + { + Sawmill.Error($"Tried to subscribe an unknown CVar '{name}.'"); + return; + } + + if (!cVar.Flags.HasFlag(CVar.REPLICATED) || !cVar.Flags.HasFlag(CVar.CLIENT)) + { + Sawmill.Error($"Tried to subscribe client cvar '{name}' without flags CLIENT | REPLICATED"); + return; + } + using (Lock.WriteGuard()) { if (!_replicatedInvokes.TryGetValue(name, out var cVarInvokes)) @@ -229,6 +241,18 @@ public override void OnClientCVarChanges(string name, Action public override void UnsubClientCVarChanges(string name, Action onValueChanged, Action? onDisconnect) { + if (!_configVars.TryGetValue(name, out var cVar)) + { + Sawmill.Error($"Tried to subscribe an unknown CVar '{name}.'"); + return; + } + + if (!cVar.Flags.HasFlag(CVar.REPLICATED) || !cVar.Flags.HasFlag(CVar.CLIENT)) + { + Sawmill.Error($"Tried to subscribe client cvar '{name}' without flags CLIENT | REPLICATED"); + return; + } + using (Lock.WriteGuard()) { if (!_replicatedInvokes.TryGetValue(name, out var cVarInvokes)) From fb86b83c32d162ad2f00d63de34ee923c23587bf Mon Sep 17 00:00:00 2001 From: Anri Date: Thu, 4 Dec 2025 22:47:17 +0300 Subject: [PATCH 07/10] copy-pasta-yummers --- Robust.Server/Configuration/ServerNetConfigurationManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Robust.Server/Configuration/ServerNetConfigurationManager.cs b/Robust.Server/Configuration/ServerNetConfigurationManager.cs index 2dd21717595..baa95818fff 100644 --- a/Robust.Server/Configuration/ServerNetConfigurationManager.cs +++ b/Robust.Server/Configuration/ServerNetConfigurationManager.cs @@ -243,13 +243,13 @@ public override void UnsubClientCVarChanges(string name, Action Date: Mon, 22 Dec 2025 11:20:47 +0300 Subject: [PATCH 08/10] remove-on-disconnect --- .../ClientNetConfigurationManager.cs | 4 +- .../ServerNetConfigurationManager.cs | 40 ++----------------- .../Configuration/NetConfigurationManager.cs | 16 ++++---- 3 files changed, 13 insertions(+), 47 deletions(-) diff --git a/Robust.Client/Configuration/ClientNetConfigurationManager.cs b/Robust.Client/Configuration/ClientNetConfigurationManager.cs index b7781286a01..2d60b9d5130 100644 --- a/Robust.Client/Configuration/ClientNetConfigurationManager.cs +++ b/Robust.Client/Configuration/ClientNetConfigurationManager.cs @@ -140,7 +140,7 @@ private void ApplyClientNetVarChange(List<(string name, object value)> networked /// public override T GetClientCVar(INetChannel channel, string name) => GetCVar(name); - public override void OnClientCVarChanges(string name, Action onChanged, Action? onDisconnect) + public override void OnClientCVarChanges(string name, Action onChanged) { if (_player.LocalSession is not { } localSession) { @@ -151,7 +151,7 @@ public override void OnClientCVarChanges(string name, Action(name, (x) => onChanged(x, localSession), true); } - public override void UnsubClientCVarChanges(string name, Action onChanged, Action? onDisconnect) + public override void UnsubClientCVarChanges(string name, Action onChanged) { if (_player.LocalSession is not { } localSession) { diff --git a/Robust.Server/Configuration/ServerNetConfigurationManager.cs b/Robust.Server/Configuration/ServerNetConfigurationManager.cs index baa95818fff..185f14b3169 100644 --- a/Robust.Server/Configuration/ServerNetConfigurationManager.cs +++ b/Robust.Server/Configuration/ServerNetConfigurationManager.cs @@ -30,14 +30,12 @@ public override void SetupNetworking() base.SetupNetworking(); NetManager.Connected += PeerConnected; NetManager.Disconnect += PeerDisconnected; - _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; } public override void Shutdown() { base.Shutdown(); - _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged; _replicatedCVars.Clear(); _replicatedInvokes.Clear(); @@ -53,27 +51,6 @@ private void PeerDisconnected(object? sender, NetDisconnectedArgs e) _replicatedCVars.Remove(e.Channel); } - private void OnPlayerStatusChanged(object? _, SessionStatusEventArgs args) - { - if (args.NewStatus != SessionStatus.Disconnected) - return; - - foreach (var (_, cVarInvoke) in _replicatedInvokes) - { - foreach (var entry in cVarInvoke.DisconnectDelegate.Entries) - { - try - { - entry.Value!.Invoke(args.Session); - } - catch (Exception e) - { - Sawmill.Error($"Error while running {nameof(DisconnectDelegate)} for replicated CVars callback: {e}"); - } - } - } - } - /// public override T GetClientCVar(INetChannel channel, string name) { @@ -203,7 +180,7 @@ private void InvokeClientCvarChange(CVarChangeInfo info, INetChannel msgChannel) } /// - public override void OnClientCVarChanges(string name, Action onValueChanged, Action? onDisconnect) + public override void OnClientCVarChanges(string name, Action onValueChanged) { if (!_configVars.TryGetValue(name, out var cVar)) { @@ -213,7 +190,7 @@ public override void OnClientCVarChanges(string name, Action(string name, Action onValueChanged((T)value, session), onValueChanged); } - - if (onDisconnect is null) - return; - - cVarInvokes.DisconnectDelegate.AddInPlace(session => onDisconnect(session), onDisconnect); } } /// - public override void UnsubClientCVarChanges(string name, Action onValueChanged, Action? onDisconnect) + public override void UnsubClientCVarChanges(string name, Action onValueChanged) { if (!_configVars.TryGetValue(name, out var cVar)) { @@ -262,11 +234,6 @@ public override void UnsubClientCVarChanges(string name, Action(string name, Action ClientChangeInvoke = new(); - public InvokeList DisconnectDelegate = new(); } } diff --git a/Robust.Shared/Configuration/NetConfigurationManager.cs b/Robust.Shared/Configuration/NetConfigurationManager.cs index 7373c3fff9d..c06868cfc18 100644 --- a/Robust.Shared/Configuration/NetConfigurationManager.cs +++ b/Robust.Shared/Configuration/NetConfigurationManager.cs @@ -66,7 +66,7 @@ T GetClientCVar(INetChannel channel, CVarDef definition) where T : notnull /// CVar type. /// Name of the CVar. /// The delegate to run when the cvar was changed. - void OnClientCVarChanges(string name, Action onChanged, Action? onDisconnect) where T : notnull; + void OnClientCVarChanges(string name, Action onChanged) where T : notnull; /// /// Listen for an event for if the config value changes on client changes. Does nothing on client side @@ -74,8 +74,8 @@ T GetClientCVar(INetChannel channel, CVarDef definition) where T : notnull /// CVar type. /// The CVar. /// The delegate to run when the cvar was changed. - void OnClientCVarChanges(CVarDef definition, Action onChanged, Action? onDisconnect) where T : notnull - => OnClientCVarChanges(definition.Name, onChanged, onDisconnect); + void OnClientCVarChanges(CVarDef definition, Action onChanged) where T : notnull + => OnClientCVarChanges(definition.Name, onChanged); /// /// Unsubscribe an event previously registered with . @@ -83,7 +83,7 @@ void OnClientCVarChanges(CVarDef definition, Action onC /// CVar type. /// Name of the CVar. /// The delegate to run when the cvar was changed. - void UnsubClientCVarChanges(string name, Action onChanged, Action? onDisconnect) where T : notnull; + void UnsubClientCVarChanges(string name, Action onChanged) where T : notnull; /// /// Unsubscribe an event previously registered with . @@ -91,8 +91,8 @@ void OnClientCVarChanges(CVarDef definition, Action onC /// CVar type. /// The CVar. /// The delegate to run when the cvar was changed. - void UnsubClientCVarChanges(CVarDef definition, Action onChanged, Action? onDisconnect) where T : notnull - => UnsubClientCVarChanges(definition.Name, onChanged, onDisconnect); + void UnsubClientCVarChanges(CVarDef definition, Action onChanged) where T : notnull + => UnsubClientCVarChanges(definition.Name, onChanged); } @@ -237,8 +237,8 @@ public void SyncConnectingClient(INetChannel client) /// public abstract T GetClientCVar(INetChannel channel, string name); - public abstract void OnClientCVarChanges(string name, Action onChanged, Action? onDisconnect) where T : notnull; + public abstract void OnClientCVarChanges(string name, Action onChanged) where T : notnull; - public abstract void UnsubClientCVarChanges(string name, Action onChanged, Action? onDisconnect) where T : notnull; + public abstract void UnsubClientCVarChanges(string name, Action onChanged) where T : notnull; } } From 6ebca962db5b860a41f230ef60af45021228f523 Mon Sep 17 00:00:00 2001 From: Anri Date: Mon, 22 Dec 2025 15:19:07 +0300 Subject: [PATCH 09/10] add-changed-cvar-info-and-fix-test --- .../ClientNetConfigurationManager.cs | 24 ++++ .../ServerNetConfigurationManager.cs | 64 ++++++++- .../NetConfigurationManagerTest.cs | 129 ++---------------- .../Configuration/NetConfigurationManager.cs | 21 ++- 4 files changed, 112 insertions(+), 126 deletions(-) diff --git a/Robust.Client/Configuration/ClientNetConfigurationManager.cs b/Robust.Client/Configuration/ClientNetConfigurationManager.cs index 2d60b9d5130..0381e664fe8 100644 --- a/Robust.Client/Configuration/ClientNetConfigurationManager.cs +++ b/Robust.Client/Configuration/ClientNetConfigurationManager.cs @@ -151,6 +151,19 @@ public override void OnClientCVarChanges(string name, Action(name, (x) => onChanged(x, localSession), true); } + /// + public override void OnClientCVarChanges(string name, ClientCVarChanged onChanged) + { + if (_player.LocalSession is not { } localSession) + { + Sawmill.Error("Got null local session for client!"); + return; + } + + OnValueChanged(name, (T newValue, in CVarChangeInfo info) => onChanged(localSession, newValue, in info), true); + } + + /// public override void UnsubClientCVarChanges(string name, Action onChanged) { if (_player.LocalSession is not { } localSession) @@ -162,4 +175,15 @@ public override void UnsubClientCVarChanges(string name, Action(name, (x) => onChanged(x, localSession)); } + /// + public override void UnsubClientCVarChanges(string name, ClientCVarChanged onChanged) + { + if (_player.LocalSession is not { } localSession) + { + Sawmill.Error("Got null local session for client!"); + return; + } + + UnsubValueChanged(name, (T newValue, in CVarChangeInfo info) => onChanged(localSession, newValue, in info)); + } } diff --git a/Robust.Server/Configuration/ServerNetConfigurationManager.cs b/Robust.Server/Configuration/ServerNetConfigurationManager.cs index 185f14b3169..e02b250a45e 100644 --- a/Robust.Server/Configuration/ServerNetConfigurationManager.cs +++ b/Robust.Server/Configuration/ServerNetConfigurationManager.cs @@ -180,7 +180,7 @@ private void InvokeClientCvarChange(CVarChangeInfo info, INetChannel msgChannel) } /// - public override void OnClientCVarChanges(string name, Action onValueChanged) + public override void OnClientCVarChanges(string name, Action onChanged) { if (!_configVars.TryGetValue(name, out var cVar)) { @@ -199,13 +199,44 @@ public override void OnClientCVarChanges(string name, Action onValueChanged((T)value, session), onValueChanged); + cVarInvokes.ClientChangeInvoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo _) => onChanged((T)value, session), onChanged); _replicatedInvokes.Add(name, cVarInvokes); } else { - cVarInvokes.ClientChangeInvoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo _) => onValueChanged((T)value, session), onValueChanged); + cVarInvokes.ClientChangeInvoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo _) => onChanged((T)value, session), onChanged); + } + } + } + + /// + public override void OnClientCVarChanges(string name, ClientCVarChanged onChanged) + { + if (!_configVars.TryGetValue(name, out var cVar)) + { + Sawmill.Error($"Tried to subscribe an unknown CVar '{name}.'"); + return; + } + + if (!cVar.Flags.HasFlag(CVar.REPLICATED) || !cVar.Flags.HasFlag(CVar.CLIENT)) + { + Sawmill.Error($"Tried to subscribe server to client cvar '{name}' but cvar don't have flags CLIENT | REPLICATED"); + return; + } + + using (Lock.WriteGuard()) + { + if (!_replicatedInvokes.TryGetValue(name, out var cVarInvokes)) + { + cVarInvokes = new ReplicatedCVarInvokes { }; + cVarInvokes.ClientChangeInvoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo info) => onChanged(session, (T)value, info), onChanged); + + _replicatedInvokes.Add(name, cVarInvokes); + } + else + { + cVarInvokes.ClientChangeInvoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo info) => onChanged(session, (T)value , info), onChanged); } } } @@ -237,6 +268,33 @@ public override void UnsubClientCVarChanges(string name, Action + public override void UnsubClientCVarChanges(string name, ClientCVarChanged onChanged) + { + if (!_configVars.TryGetValue(name, out var cVar)) + { + Sawmill.Error($"Tried to unsubscribe an unknown CVar '{name}.'"); + return; + } + + if (!cVar.Flags.HasFlag(CVar.REPLICATED) || !cVar.Flags.HasFlag(CVar.CLIENT)) + { + Sawmill.Error($"Tried to unsubscribe client cvar '{name}' without flags CLIENT | REPLICATED"); + return; + } + + using (Lock.WriteGuard()) + { + if (!_replicatedInvokes.TryGetValue(name, out var cVarInvokes)) + { + Sawmill.Warning($"Trying to unsubscribe for cvar {name} changes that dont have any subscriptions at all!"); + return; + } + + cVarInvokes.ClientChangeInvoke.RemoveInPlace(onChanged); + } + } + private delegate void DisconnectDelegate(ICommonSession session); private sealed class ReplicatedCVarInvokes diff --git a/Robust.Shared.IntegrationTests/Configuration/NetConfigurationManagerTest.cs b/Robust.Shared.IntegrationTests/Configuration/NetConfigurationManagerTest.cs index e3b216c5c32..5b475f330fa 100644 --- a/Robust.Shared.IntegrationTests/Configuration/NetConfigurationManagerTest.cs +++ b/Robust.Shared.IntegrationTests/Configuration/NetConfigurationManagerTest.cs @@ -72,7 +72,7 @@ void ClientValueChanged(int value, ICommonSession session) // actually subscribe server.Post(() => { - serverNetConfiguration.OnClientCVarChanges(CVarName, ClientValueChanged, null); + serverNetConfiguration.OnClientCVarChanges(CVarName, ClientValueChanged); }); // set new value in client @@ -99,7 +99,7 @@ void ClientValueChanged(int value, ICommonSession session) // unsubscribe server.Post(() => { - serverNetConfiguration.UnsubClientCVarChanges(CVarName, ClientValueChanged, null); + serverNetConfiguration.UnsubClientCVarChanges(CVarName, ClientValueChanged); }); // set new value in client @@ -121,96 +121,6 @@ void ClientValueChanged(int value, ICommonSession session) Assert.That(timesRan, Is.EqualTo(1)); Assert.That(SubscribeValue, Is.EqualTo(NewValue)); }); - - // now check how disconnect subscribe works - ICommonSession? disconnectSession = null; - var disconnectTimesRun = 0; - void OnDisconnect(ICommonSession session) - { - disconnectSession = session; - disconnectTimesRun++; - } - - server.Post(() => - { - serverNetConfiguration.OnClientCVarChanges(CVarName, ClientValueChanged, OnDisconnect); - }); - - // change value in client - client.Post(() => - { - clientNetConfiguration.SetCVar(CVarName, DefaultValue); - }); - - await RunTicks(server, client); - - // disconnect event don't fire on changing CVar - Assert.Multiple(() => - { - Assert.That(disconnectTimesRun, Is.EqualTo(0)); - Assert.That(disconnectSession, Is.EqualTo(null)); - }); - - // disconnect - await client.WaitPost(() => client.Resolve().ClientDisconnect("")); - - await RunTicks(server, client); - - Assert.Multiple(() => - { - Assert.That(disconnectTimesRun, Is.EqualTo(1)); - Assert.That(disconnectSession, Is.EqualTo(session)); - }); - - // reset for proper handling assertions and prevent colliding with new session - disconnectSession = null; - // again connect client - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - await client.WaitPost(() => - { - client.Resolve().ClientConnect(null!, 0, null!); - }); - - await RunTicks(server, client); - session = server.PlayerMan.Sessions.First(); - - // also check if somehow disconnectSession not null and have a strange session - Assert.Multiple(() => - { - Assert.That(disconnectTimesRun, Is.EqualTo(1)); - Assert.That(disconnectSession, Is.EqualTo(null)); - Assert.That(disconnectSession, Is.Not.EqualTo(session)); - }); - - // now unsubscribe - server.Post(() => - { - serverNetConfiguration.UnsubClientCVarChanges(CVarName, ClientValueChanged, OnDisconnect); - }); - - await RunTicks(server, client); - - // for current logic this shouldn't fire disconnect event. - Assert.Multiple(() => - { - Assert.That(disconnectTimesRun, Is.EqualTo(1)); - Assert.That(disconnectSession, Is.EqualTo(null)); - Assert.That(disconnectSession, Is.Not.EqualTo(session)); - }); - - // disconnect - await client.WaitPost(() => client.Resolve().ClientDisconnect("")); - - await RunTicks(server, client); - - // assert that unsubscribed event wasn't fired - Assert.Multiple(() => - { - Assert.That(disconnectTimesRun, Is.EqualTo(1)); - Assert.That(disconnectSession, Is.EqualTo(null)); - Assert.That(disconnectSession, Is.Not.EqualTo(session)); - }); - } [Test] @@ -233,17 +143,11 @@ void ClientValueChanged(int value, ICommonSession session) clientValues[session] = value; } - HashSet eventDisconnectedSessions = []; - void OnDisconnect(ICommonSession session) - { - eventDisconnectedSessions.Add(session); - } - // setup debug CVar server.Post(() => { server.Resolve().RegisterCVar(CVarName, DefaultValue, CVarFlags); - server.Resolve().OnClientCVarChanges(CVarName, ClientValueChanged, OnDisconnect); + server.Resolve().OnClientCVarChanges(CVarName, ClientValueChanged); }); @@ -278,10 +182,10 @@ await client.WaitPost(() => // server invoke subscribed events of replicated CVar on client connect Assert.Multiple(() => { - Assert.That(eventSessions.Count, Is.EqualTo(ClientAmount)); - Assert.That(clientValues.Count, Is.EqualTo(ClientAmount)); + Assert.That(eventSessions, Has.Count.EqualTo(ClientAmount)); + Assert.That(clientValues, Has.Count.EqualTo(ClientAmount)); - Assert.That(clientValues.Values.Distinct().Count, Is.EqualTo(1)); + Assert.That(clientValues.Values.Distinct().Count(), Is.EqualTo(1)); Assert.That(clientValues.Values.Distinct().First(), Is.EqualTo(DefaultValue)); }); @@ -305,8 +209,7 @@ await client.WaitPost(() => Assert.Multiple(() => { // session events worked correctly (reminder: last one haven't changed it CVar) - Assert.That(eventSessions.Count, Is.EqualTo(ClientAmount - 1)); - Assert.That(eventDisconnectedSessions.Count, Is.EqualTo(0)); + Assert.That(eventSessions, Has.Count.EqualTo(ClientAmount - 1)); for (int i = 0; i < ClientAmount - 1; i++) { @@ -320,24 +223,8 @@ await client.WaitPost(() => } var lastSession = clientSessions[ClientAmount - 1]; - Assert.That(clientValues.ContainsKey(lastSession), Is.EqualTo(false)); + Assert.That(clientValues.ContainsKey(lastSession), Is.False); }); - - for (int i = 0; i < ClientAmount; i++) - { - var client = clients[i]; - - await client.WaitPost(() => client.Resolve().ClientDisconnect("")); - - await RunTicks(server, client); - - // assert that every disconnect result in event raising - Assert.That(eventDisconnectedSessions.Count, Is.EqualTo(i + 1)); - } - - // we received same sessions EXCEPT last one - Assert.That(eventDisconnectedSessions.Except(eventSessions), Is.EqualTo(new HashSet { clientSessions[ClientAmount - 1] })); - Assert.That(clientSessions, Is.EqualTo(eventDisconnectedSessions)); } private async Task RunTicks(IntegrationInstance server, IntegrationInstance client, int numberOfTicks = 5) diff --git a/Robust.Shared/Configuration/NetConfigurationManager.cs b/Robust.Shared/Configuration/NetConfigurationManager.cs index c06868cfc18..60af0942ddc 100644 --- a/Robust.Shared/Configuration/NetConfigurationManager.cs +++ b/Robust.Shared/Configuration/NetConfigurationManager.cs @@ -11,6 +11,8 @@ namespace Robust.Shared.Configuration { + public delegate void ClientCVarChanged(ICommonSession session, T newValue, in CVarChangeInfo info); + /// /// A networked configuration manager that controls the replication of /// console variables between client and server. @@ -61,7 +63,7 @@ T GetClientCVar(INetChannel channel, CVarDef definition) where T : notnull => GetClientCVar(channel, definition.Name); /// - /// Listen for an event for if the config value changes on client changes. Does nothing on client side + /// Listen for an event for if the config value changes on client changes /// /// CVar type. /// Name of the CVar. @@ -69,7 +71,7 @@ T GetClientCVar(INetChannel channel, CVarDef definition) where T : notnull void OnClientCVarChanges(string name, Action onChanged) where T : notnull; /// - /// Listen for an event for if the config value changes on client changes. Does nothing on client side + /// Listen for an event for if the config value changes on client changes /// /// CVar type. /// The CVar. @@ -77,6 +79,13 @@ T GetClientCVar(INetChannel channel, CVarDef definition) where T : notnull void OnClientCVarChanges(CVarDef definition, Action onChanged) where T : notnull => OnClientCVarChanges(definition.Name, onChanged); + /// + void OnClientCVarChanges(string name, ClientCVarChanged onChanged) where T : notnull; + + /// + void OnClientCVarChanges(CVarDef definition, ClientCVarChanged onChanged) where T : notnull + => OnClientCVarChanges(definition.Name, onChanged); + /// /// Unsubscribe an event previously registered with . /// @@ -94,6 +103,12 @@ void OnClientCVarChanges(CVarDef definition, Action onC void UnsubClientCVarChanges(CVarDef definition, Action onChanged) where T : notnull => UnsubClientCVarChanges(definition.Name, onChanged); + /// + void UnsubClientCVarChanges(string name, ClientCVarChanged onChanged) where T : notnull; + + /// + void UnsubClientCVarChanges(CVarDef definition, ClientCVarChanged onChanged) where T : notnull + => UnsubClientCVarChanges(definition.Name, onChanged); } internal interface INetConfigurationManagerInternal : INetConfigurationManager, IConfigurationManagerInternal @@ -238,7 +253,9 @@ public void SyncConnectingClient(INetChannel client) public abstract T GetClientCVar(INetChannel channel, string name); public abstract void OnClientCVarChanges(string name, Action onChanged) where T : notnull; + public abstract void OnClientCVarChanges(string name, ClientCVarChanged onChanged) where T : notnull; public abstract void UnsubClientCVarChanges(string name, Action onChanged) where T : notnull; + public abstract void UnsubClientCVarChanges(string name, ClientCVarChanged onChanged) where T : notnull; } } From 741042d309a5dc35780bff1a038cf79f6b9efe5f Mon Sep 17 00:00:00 2001 From: Anri Date: Mon, 22 Dec 2025 15:23:09 +0300 Subject: [PATCH 10/10] fix-ide-moment --- Robust.Server/Configuration/ServerNetConfigurationManager.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Robust.Server/Configuration/ServerNetConfigurationManager.cs b/Robust.Server/Configuration/ServerNetConfigurationManager.cs index e02b250a45e..f3a5a2ff18e 100644 --- a/Robust.Server/Configuration/ServerNetConfigurationManager.cs +++ b/Robust.Server/Configuration/ServerNetConfigurationManager.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; using Robust.Server.Player; using Robust.Shared.Collections; using Robust.Shared.Configuration; -using Robust.Shared.Enums; using Robust.Shared.IoC; using Robust.Shared.Network; using Robust.Shared.Network.Messages; @@ -11,6 +8,8 @@ using Robust.Shared.Replays; using Robust.Shared.Timing; using Robust.Shared.Utility; +using System; +using System.Collections.Generic; namespace Robust.Server.Configuration;