diff --git a/Robust.Client/Configuration/ClientNetConfigurationManager.cs b/Robust.Client/Configuration/ClientNetConfigurationManager.cs index 8722cb1049a..0381e664fe8 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,51 @@ 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 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) + { + Sawmill.Error("Got null local session for client!"); + return; + } + + UnsubValueChanged(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 61e52181811..f3a5a2ff18e 100644 --- a/Robust.Server/Configuration/ServerNetConfigurationManager.cs +++ b/Robust.Server/Configuration/ServerNetConfigurationManager.cs @@ -1,11 +1,15 @@ +using Robust.Server.Player; +using Robust.Shared.Collections; using Robust.Shared.Configuration; +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; using System.Collections.Generic; -using Robust.Shared.IoC; -using Robust.Shared.Replays; namespace Robust.Server.Configuration; @@ -13,9 +17,13 @@ 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 _replicatedInvokes = new(); + + public override void SetupNetworking() { base.SetupNetworking(); @@ -26,7 +34,10 @@ public override void SetupNetworking() public override void Shutdown() { base.Shutdown(); + + _replicatedCVars.Clear(); + _replicatedInvokes.Clear(); } private void PeerConnected(object? sender, NetChannelArgs e) @@ -106,31 +117,187 @@ protected override void ApplyNetVarChange( return; } - using var _ = Lock.ReadGuard(); + var cVarChanges = new List(); + using (Lock.ReadGuard()) + { + foreach (var (name, value) in networkedVars) + { + 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; + } - foreach (var (name, value) in networkedVars) + 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); + } + } + + private void InvokeClientCvarChange(CVarChangeInfo info, INetChannel msgChannel) + { + if (!_playerManager.TryGetSessionByChannel(msgChannel, out var session)) { - if (!_configVars.TryGetValue(name, out var cVar)) + Sawmill.Error($"Got client cvar change for NetChannel {msgChannel.UserId} without session!"); + return; + } + + if (!_replicatedInvokes.TryGetValue(info.Name, out var cVarInvokes)) + return; + + foreach (var entry in cVarInvokes.ClientChangeInvoke.Entries) + { + try + { + entry.Value!.Invoke(info.NewValue, session, in info); + } + catch (Exception e) { - Sawmill.Warning($"{msgChannel} tried to replicate an unknown CVar '{name}.'"); - continue; + Sawmill.Error($"Error while running {nameof(ClientValueChangedDelegate)} for replicated CVars callback: {e}"); } + } + } + + /// + public override void OnClientCVarChanges(string name, Action onChanged) + { + if (!_configVars.TryGetValue(name, out var cVar)) + { + Sawmill.Error($"Tried to subscribe an unknown CVar '{name}.'"); + return; + } - if (!cVar.Registered) + 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 _) => onChanged((T)value, session), onChanged); + + _replicatedInvokes.Add(name, cVarInvokes); + } + else { - Sawmill.Warning($"{msgChannel} tried to replicate an unregistered CVar '{name}.'"); - continue; + 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 & CVar.REPLICATED) != 0) + 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)) { - clientCVars[name] = value; - Sawmill.Debug($"name={name}, val={value}"); + cVarInvokes = new ReplicatedCVarInvokes { }; + cVarInvokes.ClientChangeInvoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo info) => onChanged(session, (T)value, info), onChanged); + + _replicatedInvokes.Add(name, cVarInvokes); } else { - Sawmill.Warning($"{msgChannel} tried to replicate an un-replicated CVar '{name}.'"); + cVarInvokes.ClientChangeInvoke.AddInPlace((object value, ICommonSession session, in CVarChangeInfo info) => onChanged(session, (T)value , info), onChanged); } } } + + /// + public override void UnsubClientCVarChanges(string name, Action onValueChanged) + { + 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(onValueChanged); + } + } + + /// + 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 + { + public InvokeList ClientChangeInvoke = new(); + } } diff --git a/Robust.Shared.IntegrationTests/Configuration/ConfigurationManagerTest.cs b/Robust.Shared.IntegrationTests/Configuration/ConfigurationManagerTest.cs index ef84ada7194..744b2a42217 100644 --- a/Robust.Shared.IntegrationTests/Configuration/ConfigurationManagerTest.cs +++ b/Robust.Shared.IntegrationTests/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.Shared.IntegrationTests/Configuration/NetConfigurationManagerTest.cs b/Robust.Shared.IntegrationTests/Configuration/NetConfigurationManagerTest.cs new file mode 100644 index 00000000000..5b475f330fa --- /dev/null +++ b/Robust.Shared.IntegrationTests/Configuration/NetConfigurationManagerTest.cs @@ -0,0 +1,239 @@ +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); + }); + + // 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); + }); + + // 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)); + }); + } + + [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; + } + + // setup debug CVar + server.Post(() => + { + server.Resolve().RegisterCVar(CVarName, DefaultValue, CVarFlags); + server.Resolve().OnClientCVarChanges(CVarName, ClientValueChanged); + + }); + + // 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, 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().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, Has.Count.EqualTo(ClientAmount - 1)); + + 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.False); + }); + } + + 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); + } + } +} + diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityState_Tests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityState_Tests.cs index e042a1aa59a..3686eb0eab9 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityState_Tests.cs +++ b/Robust.Shared.IntegrationTests/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.Shared.IntegrationTests/GameObjects/EntitySystemManagerOrderTest.cs b/Robust.Shared.IntegrationTests/GameObjects/EntitySystemManagerOrderTest.cs index 8cf31f52881..849cb603542 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntitySystemManagerOrderTest.cs +++ b/Robust.Shared.IntegrationTests/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); diff --git a/Robust.Shared/Configuration/NetConfigurationManager.cs b/Robust.Shared/Configuration/NetConfigurationManager.cs index f13c0e71491..60af0942ddc 100644 --- a/Robust.Shared/Configuration/NetConfigurationManager.cs +++ b/Robust.Shared/Configuration/NetConfigurationManager.cs @@ -5,11 +5,14 @@ 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; 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. @@ -58,6 +61,54 @@ 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 + /// + /// 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 + /// + /// 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(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 . + /// + /// 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); + + /// + 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 @@ -65,6 +116,8 @@ internal interface INetConfigurationManagerInternal : INetConfigurationManager, } + public delegate void ClientValueChangedDelegate(object value, ICommonSession session, in CVarChangeInfo info); + /// internal abstract class NetConfigurationManager : ConfigurationManager, INetConfigurationManagerInternal { @@ -198,5 +251,11 @@ 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; } }