From 9c2af43af68485dd3096418645d82d1418d35ab4 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:21:44 +1200 Subject: [PATCH] Deduplicate & fix prediction reconciliation test (#18635) --- .../Networking/AutoPredictReconcileTest.cs | 454 ----------------- .../Networking/SimplePredictReconcileTest.cs | 149 +++--- .../Networking/SystemPredictReconcileTest.cs | 473 ------------------ .../Administration/Systems/AdminVerbSystem.cs | 16 +- .../Anomaly/AnomalySystem.Scanner.cs | 2 - .../Anomaly/AnomalySystem.Vessel.cs | 16 +- .../Cargo/Systems/CargoSystem.Orders.cs | 6 - .../Cargo/Systems/CargoSystem.Shuttle.cs | 1 + 8 files changed, 83 insertions(+), 1034 deletions(-) delete mode 100644 Content.IntegrationTests/Tests/Networking/AutoPredictReconcileTest.cs delete mode 100644 Content.IntegrationTests/Tests/Networking/SystemPredictReconcileTest.cs diff --git a/Content.IntegrationTests/Tests/Networking/AutoPredictReconcileTest.cs b/Content.IntegrationTests/Tests/Networking/AutoPredictReconcileTest.cs deleted file mode 100644 index 4c51e91266..0000000000 --- a/Content.IntegrationTests/Tests/Networking/AutoPredictReconcileTest.cs +++ /dev/null @@ -1,454 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Robust.Client.GameStates; -using Robust.Client.Timing; -using Robust.Server.Player; -using Robust.Shared; -using Robust.Shared.Analyzers; -using Robust.Shared.Configuration; -using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; -using Robust.Shared.IoC; -using Robust.Shared.Map; -using Robust.Shared.Timing; - -namespace Content.IntegrationTests.Tests.Networking -{ - // This test checks that the prediction & reconciling system is working correctly with a simple boolean flag. - // An entity system sets a flag on a networked component via a RaisePredictiveEvent, - // so it runs predicted on client and eventually on server. - // All the tick values are checked to ensure it arrives on client & server at the exact correct ticks. - // On the client, the reconciling system is checked to ensure that the state correctly reset every tick, - // until the server acknowledges it. - // Then, the same test is performed again, but the server does not handle the message (it ignores it). - // To simulate a mispredict. - // This means the client is forced to reset it once it gets to the server tick where the server didn't do anything. - // the tick where the server *should* have, but did not, acknowledge the state change. - // Finally, we run two events inside the prediction area to ensure reconciling does for incremental stuff. - [TestFixture] - public sealed class AutoPredictReconcileTest - { - [Test] - public async Task Test() - { - // TODO remove fresh=true. - // Instead, offset the all the explicit tick checks by some initial tick number. - await using var pairTracker = await PoolManager.GetServerClient(new() { Fresh = true, DummyTicker = true }); - var server = pairTracker.Pair.Server; - var client = pairTracker.Pair.Client; - - // Pull in all dependencies we need. - var sPlayerManager = server.ResolveDependency(); - var sMapManager = server.ResolveDependency(); - var sEntityManager = server.ResolveDependency(); - var cEntityManager = client.ResolveDependency(); - var sGameTiming = server.ResolveDependency(); - var cGameTiming = client.ResolveDependency(); - var cGameStateManager = client.ResolveDependency(); - var cfg = client.ResolveDependency(); - var log = cfg.GetCVar(CVars.NetLogging); - - //cfg.SetCVar(CVars.NetLogging, true); - - EntityUid serverEnt = default; - AutoPredictionTestComponent serverComponent = default!; - AutoPredictionTestComponent clientComponent = default!; - - var serverSystem = server.ResolveDependency() - .GetEntitySystem(); - var clientSystem = client.ResolveDependency() - .GetEntitySystem(); - - await server.WaitPost(() => - { - // Spawn dummy component entity. - var map = sMapManager.CreateMap(); - var player = sPlayerManager.ServerSessions.Single(); - serverEnt = sEntityManager.SpawnEntity(null, new MapCoordinates(new Vector2(0, 0), map)); - serverComponent = sEntityManager.AddComponent(serverEnt); - - // Make client "join game" so they receive game state updates. - player.JoinGame(); - }); - - // Run some ticks so that - await PoolManager.RunTicksSync(pairTracker.Pair, 3); - - // Check client buffer is full - Assert.That(cGameStateManager.CurrentBufferSize, Is.EqualTo(cGameStateManager.TargetBufferSize)); - - // This isn't required anymore, but the test had this for the sake of "technical things", and I cbf shifting - // all the tick times over. So it stays. - await client.WaitRunTicks(1); - - await client.WaitPost(() => - { - clientComponent = cEntityManager.GetComponent(serverEnt); - }); - - Assert.Multiple(() => - { - Assert.That(clientComponent.Foo, Is.False); - - // KEEP IN MIND WHEN READING THIS. - // The game loop increments CurTick AFTER running the tick. - // So when reading CurTick inside an Assert or Post or whatever, the tick reported is the NEXT one to run. - - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(14))); - Assert.That(serverComponent.Foo, Is.False); - - // Client last ran tick 15 meaning it's ahead of the last server tick it processed (12) - Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(16))); - Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick(12))); - }); - - // *** I am using block scopes to visually distinguish these sections of the test to make it more readable. - - - // Send an event to change the flag and instantly see the effect replicate client side, - // while it's queued on server and reconciling works (constantly needs re-firing on client). - { - await client.WaitPost(() => - { - cEntityManager.RaisePredictiveEvent(new SetFooMessage(serverEnt, true)); - - Assert.That(clientComponent.Foo, Is.True); - }); - - // Event correctly arrived on client system. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), true, false, true, true) })); - clientSystem.EventTriggerList.Clear(); - - // Two ticks happen on both sides with nothing really "changing". - // Server doesn't receive it yet, - // client is still replaying the past prediction. - for (var i = 0; i < 2; i++) - { - await server.WaitRunTicks(1); - - // Event did not arrive on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), false, false, true, true) })); - clientSystem.EventTriggerList.Clear(); - } - - { - await server.WaitRunTicks(1); - - Assert.Multiple(() => - { - // Event arrived on server at tick 16. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(17))); - Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), true, false, true, true) })); - }); - serverSystem.EventTriggerList.Clear(); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), false, false, true, true) })); - clientSystem.EventTriggerList.Clear(); - } - - { - await server.WaitRunTicks(1); - - // Nothing happened on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - Assert.Multiple(() => - { - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, Is.Empty); - Assert.That(clientComponent.Foo, Is.True); - }); - clientSystem.EventTriggerList.Clear(); - } - } - - // Disallow changes to simulate a misprediction. - serverSystem.Allow = false; - - Assert.Multiple(() => - { - // Assert timing is still correct, should be but it's a good reference for the rest of the test. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(18))); - Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(20))); - Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick(16))); - }); - - { - // Send event to server to change flag again, this time to disable it.. - await client.WaitPost(() => - { - cEntityManager.RaisePredictiveEvent(new SetFooMessage(serverEnt, false)); - - Assert.That(clientComponent.Foo, Is.False); - }); - - // Event correctly arrived on client system. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), true, true, false, false) })); - clientSystem.EventTriggerList.Clear(); - - for (var i = 0; i < 2; i++) - { - await server.WaitRunTicks(1); - - // Event did not arrive on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), false, true, false, false) })); - clientSystem.EventTriggerList.Clear(); - } - - { - await server.WaitRunTicks(1); - - Assert.Multiple(() => - { - // Event arrived on server at tick 20. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(21))); - // But the server didn't listen! - Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), true, true, true, false) })); - }); - serverSystem.EventTriggerList.Clear(); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), false, true, false, false) })); - clientSystem.EventTriggerList.Clear(); - } - - { - await server.WaitRunTicks(1); - - // Nothing happened on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - Assert.Multiple(() => - { - // Event no longer got repeated and flag was *not* set by server state. - // Mispredict gracefully handled! - Assert.That(clientSystem.EventTriggerList, Is.Empty); - Assert.That(clientComponent.Foo, Is.True); - }); - clientSystem.EventTriggerList.Clear(); - } - } - - // Re-allow changes to make everything work correctly again. - serverSystem.Allow = true; - - Assert.Multiple(() => - { - // Assert timing is still correct. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(22))); - Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(24))); - Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick(20))); - }); - - { - // Send first event to disable the flag (reminder: it never got accepted by the server). - await client.WaitPost(() => - { - cEntityManager.RaisePredictiveEvent(new SetFooMessage(serverEnt, false)); - - Assert.That(clientComponent.Foo, Is.False); - }); - - // Event correctly arrived on client system. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(24), true, true, false, false) })); - clientSystem.EventTriggerList.Clear(); - - // Run one tick, everything checks out. - { - await server.WaitRunTicks(1); - - // Event did not arrive on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(24), false, true, false, false) })); - clientSystem.EventTriggerList.Clear(); - } - - // Send another event, to re-enable it. - await client.WaitPost(() => - { - cEntityManager.RaisePredictiveEvent(new SetFooMessage(serverEnt, true)); - - Assert.That(clientComponent.Foo, Is.True); - }); - - // Event correctly arrived on client system. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(25), true, false, true, true) })); - clientSystem.EventTriggerList.Clear(); - - // Next tick we run, both events come in, but at different times. - { - await server.WaitRunTicks(1); - - // Event did not arrive on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] - { - (new GameTick(24), false, true, false, false), (new GameTick(25), false, false, true, true) - })); - clientSystem.EventTriggerList.Clear(); - } - - // FIRST event arrives on server! - { - await server.WaitRunTicks(1); - - Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(24), true, true, false, false) })); - serverSystem.EventTriggerList.Clear(); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] - { - (new GameTick(24), false, true, false, false), (new GameTick(25), false, false, true, true) - })); - clientSystem.EventTriggerList.Clear(); - } - - // SECOND event arrived on server, client receives ack for first event, - // still runs second event as past prediction. - { - await server.WaitRunTicks(1); - - Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(25), true, false, true, true) })); - serverSystem.EventTriggerList.Clear(); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] - { - (new GameTick(25), false, false, true, true) - })); - clientSystem.EventTriggerList.Clear(); - } - - // Finally, second event acknowledged on client and we're good. - { - await server.WaitRunTicks(1); - - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - Assert.Multiple(() => - { - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, Is.Empty); - - Assert.That(clientComponent.Foo, Is.True); - }); - } - } - - cfg.SetCVar(CVars.NetLogging, log); - await pairTracker.CleanReturnAsync(); - } - - public sealed class AutoPredictionTestEntitySystem : EntitySystem - { - public bool Allow { get; set; } = true; - - // Queue of all the events that come in so we can test that they come in perfectly as expected. - public List<(GameTick tick, bool firstPredict, bool old, bool @new, bool value)> EventTriggerList { get; } = - new(); - - [Dependency] private readonly IGameTiming _gameTiming = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeNetworkEvent(HandleMessage); - SubscribeLocalEvent(HandleMessage); - SubscribeLocalEvent(AfterAutoHandleState); - } - - private void HandleMessage(SetFooMessage message, EntitySessionEventArgs args) - { - var component = EntityManager.GetComponent(message.Uid); - var old = component.Foo; - if (Allow) - { - component.Foo = message.NewFoo; - Dirty(message.Uid, component); - } - - EventTriggerList.Add((_gameTiming.CurTick, _gameTiming.IsFirstTimePredicted, old, component.Foo, message.NewFoo)); - } - - private void AfterAutoHandleState(EntityUid uid, AutoPredictionTestComponent comp, ref AfterAutoHandleStateEvent args) - { - Dirty(uid, comp); - } - } - - public sealed class SetFooMessage : EntityEventArgs - { - public SetFooMessage(EntityUid uid, bool newFoo) - { - Uid = uid; - NewFoo = newFoo; - } - - public EntityUid Uid { get; } - public bool NewFoo { get; } - } - } - - // Must be directly located in the namespace or the sourcegen can't find it. - [NetworkedComponent()] - [AutoGenerateComponentState] - [Access(typeof(AutoPredictReconcileTest.AutoPredictionTestEntitySystem))] - [RegisterComponent] - public sealed partial class AutoPredictionTestComponent : Component - { - [AutoNetworkedField] - public bool Foo; - } -} diff --git a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs index fd8bb0ad5c..f0cd2747ad 100644 --- a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs +++ b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs @@ -1,10 +1,8 @@ #nullable enable using System.Collections.Generic; -using System.Linq; using System.Numerics; using Robust.Client.GameStates; using Robust.Client.Timing; -using Robust.Server.Player; using Robust.Shared; using Robust.Shared.Analyzers; using Robust.Shared.Configuration; @@ -12,7 +10,6 @@ using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Map; -using Robust.Shared.Serialization; using Robust.Shared.Timing; namespace Content.IntegrationTests.Tests.Networking @@ -28,21 +25,16 @@ namespace Content.IntegrationTests.Tests.Networking // This means the client is forced to reset it once it gets to the server tick where the server didn't do anything. // the tick where the server *should* have, but did not, acknowledge the state change. // Finally, we run two events inside the prediction area to ensure reconciling does for incremental stuff. - // TODO: This test relies on the EC version of component state handling. Remove in favor of the other two tests for the ECS and auto versions. [TestFixture] public sealed class SimplePredictReconcileTest { [Test] public async Task Test() { - // TODO remove fresh=true. - // Instead, offset the all the explicit tick checks by some initial tick number. - await using var pairTracker = await PoolManager.GetServerClient(new() { Fresh = true, DummyTicker = true }); + await using var pairTracker = await PoolManager.GetServerClient(new() { DummyTicker = true }); var server = pairTracker.Pair.Server; var client = pairTracker.Pair.Client; - // Pull in all dependencies we need. - var sPlayerManager = server.ResolveDependency(); var sMapManager = server.ResolveDependency(); var sEntityManager = server.ResolveDependency(); var cEntityManager = client.ResolveDependency(); @@ -52,37 +44,34 @@ namespace Content.IntegrationTests.Tests.Networking var cfg = client.ResolveDependency(); var log = cfg.GetCVar(CVars.NetLogging); - //cfg.SetCVar(CVars.NetLogging, true); - EntityUid serverEnt = default; PredictionTestComponent serverComponent = default!; PredictionTestComponent clientComponent = default!; - - var serverSystem = server.ResolveDependency() - .GetEntitySystem(); - var clientSystem = client.ResolveDependency() - .GetEntitySystem(); + var serverSystem = sEntityManager.System(); + var clientSystem = cEntityManager.System(); await server.WaitPost(() => { // Spawn dummy component entity. var map = sMapManager.CreateMap(); - var player = sPlayerManager.ServerSessions.Single(); serverEnt = sEntityManager.SpawnEntity(null, new MapCoordinates(new Vector2(0, 0), map)); serverComponent = sEntityManager.AddComponent(serverEnt); - - // Make client "join game" so they receive game state updates. - player.JoinGame(); }); - // Run some ticks so that - await PoolManager.RunTicksSync(pairTracker.Pair, 3); + // Run some ticks and ensure that the buffer has filled up. + await PoolManager.SyncTicks(pairTracker.Pair); + await PoolManager.RunTicksSync(pairTracker.Pair, 25); + Assert.That(cGameTiming.TickTimingAdjustment, Is.EqualTo(0)); + Assert.That(sGameTiming.TickTimingAdjustment, Is.EqualTo(0)); // Check client buffer is full Assert.That(cGameStateManager.CurrentBufferSize, Is.EqualTo(cGameStateManager.TargetBufferSize)); + Assert.That(cGameStateManager.TargetBufferSize, Is.EqualTo(2)); // This isn't required anymore, but the test had this for the sake of "technical things", and I cbf shifting // all the tick times over. So it stays. + // For the record, the old comment on this test literally just mumbled something about "Due to technical things ...". + // I love helpful comments. await client.WaitRunTicks(1); await client.WaitPost(() => @@ -90,6 +79,13 @@ namespace Content.IntegrationTests.Tests.Networking clientComponent = cEntityManager.GetComponent(serverEnt); }); + var baseTick = sGameTiming.CurTick.Value; + var delta = cGameTiming.CurTick.Value - baseTick; + Assert.That(delta, Is.EqualTo(2)); + + // When we expect the client to receive the message. + var expected = new GameTick(baseTick + delta); + Assert.Multiple(() => { Assert.That(clientComponent.Foo, Is.False); @@ -97,13 +93,11 @@ namespace Content.IntegrationTests.Tests.Networking // KEEP IN MIND WHEN READING THIS. // The game loop increments CurTick AFTER running the tick. // So when reading CurTick inside an Assert or Post or whatever, the tick reported is the NEXT one to run. - - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(14))); Assert.That(serverComponent.Foo, Is.False); // Client last ran tick 15 meaning it's ahead of the last server tick it processed (12) - Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(16))); - Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick(12))); + Assert.That(cGameTiming.CurTick, Is.EqualTo(expected)); + Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick((uint)(baseTick - cGameStateManager.TargetBufferSize)))); }); // *** I am using block scopes to visually distinguish these sections of the test to make it more readable. @@ -112,16 +106,16 @@ namespace Content.IntegrationTests.Tests.Networking // Send an event to change the flag and instantly see the effect replicate client side, // while it's queued on server and reconciling works (constantly needs re-firing on client). { + Assert.That(clientComponent.Foo, Is.False); await client.WaitPost(() => { cEntityManager.RaisePredictiveEvent(new SetFooMessage(serverEnt, true)); - - Assert.That(clientComponent.Foo, Is.True); }); + Assert.That(clientComponent.Foo, Is.True); // Event correctly arrived on client system. Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), true, false, true, true) })); + Is.EquivalentTo(new[] { (clientReceive: expected, true, false, true, true) })); clientSystem.EventTriggerList.Clear(); // Two ticks happen on both sides with nothing really "changing". @@ -138,7 +132,7 @@ namespace Content.IntegrationTests.Tests.Networking // Event got repeated on client as a past prediction. Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), false, false, true, true) })); + Is.EquivalentTo(new[] { (clientReceive: expected, false, false, true, true) })); clientSystem.EventTriggerList.Clear(); } @@ -148,9 +142,9 @@ namespace Content.IntegrationTests.Tests.Networking Assert.Multiple(() => { // Event arrived on server at tick 16. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(17))); + Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 3))); Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), true, false, true, true) })); + Is.EquivalentTo(new[] { (clientReceive: expected, true, false, true, true) })); }); serverSystem.EventTriggerList.Clear(); @@ -158,7 +152,7 @@ namespace Content.IntegrationTests.Tests.Networking // Event got repeated on client as a past prediction. Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), false, false, true, true) })); + Is.EquivalentTo(new[] { (clientReceive: expected, false, false, true, true) })); clientSystem.EventTriggerList.Clear(); } @@ -186,9 +180,9 @@ namespace Content.IntegrationTests.Tests.Networking Assert.Multiple(() => { // Assert timing is still correct, should be but it's a good reference for the rest of the test. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(18))); - Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(20))); - Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick(16))); + Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 4))); + Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 4 + delta))); + Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(expected)); }); { @@ -202,7 +196,7 @@ namespace Content.IntegrationTests.Tests.Networking // Event correctly arrived on client system. Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), true, true, false, false) })); + Is.EquivalentTo(new[] { (new GameTick(baseTick + 6), true, true, false, false) })); clientSystem.EventTriggerList.Clear(); for (var i = 0; i < 2; i++) @@ -216,7 +210,7 @@ namespace Content.IntegrationTests.Tests.Networking // Event got repeated on client as a past prediction. Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), false, true, false, false) })); + Is.EquivalentTo(new[] { (new GameTick(baseTick + 6), false, true, false, false) })); clientSystem.EventTriggerList.Clear(); } @@ -226,10 +220,10 @@ namespace Content.IntegrationTests.Tests.Networking Assert.Multiple(() => { // Event arrived on server at tick 20. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(21))); + Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 7))); // But the server didn't listen! Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), true, true, true, false) })); + Is.EquivalentTo(new[] { (new GameTick(baseTick + 6), true, true, true, false) })); }); serverSystem.EventTriggerList.Clear(); @@ -237,7 +231,7 @@ namespace Content.IntegrationTests.Tests.Networking // Event got repeated on client as a past prediction. Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), false, true, false, false) })); + Is.EquivalentTo(new[] { (new GameTick(baseTick + 6), false, true, false, false) })); clientSystem.EventTriggerList.Clear(); } @@ -266,9 +260,9 @@ namespace Content.IntegrationTests.Tests.Networking Assert.Multiple(() => { // Assert timing is still correct. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(22))); - Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(24))); - Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick(20))); + Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 8))); + Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 8 + delta))); + Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick((uint)(baseTick + 8 - cGameStateManager.TargetBufferSize)))); }); { @@ -282,7 +276,7 @@ namespace Content.IntegrationTests.Tests.Networking // Event correctly arrived on client system. Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(24), true, true, false, false) })); + Is.EquivalentTo(new[] { (new GameTick(baseTick + 10), true, true, false, false) })); clientSystem.EventTriggerList.Clear(); // Run one tick, everything checks out. @@ -296,7 +290,7 @@ namespace Content.IntegrationTests.Tests.Networking // Event got repeated on client as a past prediction. Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(24), false, true, false, false) })); + Is.EquivalentTo(new[] { (new GameTick(baseTick + 10), false, true, false, false) })); clientSystem.EventTriggerList.Clear(); } @@ -310,7 +304,7 @@ namespace Content.IntegrationTests.Tests.Networking // Event correctly arrived on client system. Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(25), true, false, true, true) })); + Is.EquivalentTo(new[] { (new GameTick(baseTick + 11), true, false, true, true) })); clientSystem.EventTriggerList.Clear(); // Next tick we run, both events come in, but at different times. @@ -326,7 +320,7 @@ namespace Content.IntegrationTests.Tests.Networking Assert.That(clientSystem.EventTriggerList, Is.EquivalentTo(new[] { - (new GameTick(24), false, true, false, false), (new GameTick(25), false, false, true, true) + (new GameTick(baseTick + 10), false, true, false, false), (new GameTick(baseTick + 11), false, false, true, true) })); clientSystem.EventTriggerList.Clear(); } @@ -336,7 +330,7 @@ namespace Content.IntegrationTests.Tests.Networking await server.WaitRunTicks(1); Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(24), true, true, false, false) })); + Is.EquivalentTo(new[] { (new GameTick(baseTick + 10), true, true, false, false) })); serverSystem.EventTriggerList.Clear(); await client.WaitRunTicks(1); @@ -345,7 +339,7 @@ namespace Content.IntegrationTests.Tests.Networking Assert.That(clientSystem.EventTriggerList, Is.EquivalentTo(new[] { - (new GameTick(24), false, true, false, false), (new GameTick(25), false, false, true, true) + (new GameTick(baseTick + 10), false, true, false, false), (new GameTick(baseTick + 11), false, false, true, true) })); clientSystem.EventTriggerList.Clear(); } @@ -356,7 +350,7 @@ namespace Content.IntegrationTests.Tests.Networking await server.WaitRunTicks(1); Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(25), true, false, true, true) })); + Is.EquivalentTo(new[] { (new GameTick(baseTick + 11), true, false, true, true) })); serverSystem.EventTriggerList.Clear(); await client.WaitRunTicks(1); @@ -365,7 +359,7 @@ namespace Content.IntegrationTests.Tests.Networking Assert.That(clientSystem.EventTriggerList, Is.EquivalentTo(new[] { - (new GameTick(25), false, false, true, true) + (new GameTick(baseTick + 11), false, false, true, true) })); clientSystem.EventTriggerList.Clear(); } @@ -392,27 +386,8 @@ namespace Content.IntegrationTests.Tests.Networking await pairTracker.CleanReturnAsync(); } - [NetworkedComponent()] - [Access(typeof(PredictionTestEntitySystem))] - [RegisterComponent] - public sealed class PredictionTestComponent : Component - { - public bool Foo; - } - public sealed class PredictionTestEntitySystem : EntitySystem { - [Serializable, NetSerializable] - private sealed class PredictionComponentState : ComponentState - { - public bool Foo { get; } - - public PredictionComponentState(bool foo) - { - Foo = foo; - } - } - public bool Allow { get; set; } = true; // Queue of all the events that come in so we can test that they come in perfectly as expected. @@ -425,25 +400,7 @@ namespace Content.IntegrationTests.Tests.Networking { base.Initialize(); - SubscribeNetworkEvent(HandleMessage); - SubscribeLocalEvent(HandleMessage); - - SubscribeLocalEvent(OnGetState); - SubscribeLocalEvent(OnHandleState); - } - - private void OnHandleState(EntityUid uid, PredictionTestComponent component, ref ComponentHandleState args) - { - if (args.Current is not PredictionComponentState state) - return; - - component.Foo = state.Foo; - Dirty(component); - } - - private void OnGetState(EntityUid uid, PredictionTestComponent component, ref ComponentGetState args) - { - args.State = new PredictionComponentState(component.Foo); + SubscribeAllEvent(HandleMessage); } private void HandleMessage(SetFooMessage message, EntitySessionEventArgs args) @@ -460,7 +417,7 @@ namespace Content.IntegrationTests.Tests.Networking } } - private sealed class SetFooMessage : EntityEventArgs + public sealed class SetFooMessage : EntityEventArgs { public SetFooMessage(EntityUid uid, bool newFoo) { @@ -472,4 +429,14 @@ namespace Content.IntegrationTests.Tests.Networking public bool NewFoo { get; } } } + + // Must be directly located in the namespace or the sourcegen can't find it. + [NetworkedComponent] + [AutoGenerateComponentState] + [RegisterComponent] + public sealed partial class PredictionTestComponent : Component + { + [AutoNetworkedField] + public bool Foo; + } } diff --git a/Content.IntegrationTests/Tests/Networking/SystemPredictReconcileTest.cs b/Content.IntegrationTests/Tests/Networking/SystemPredictReconcileTest.cs deleted file mode 100644 index 68a48126ad..0000000000 --- a/Content.IntegrationTests/Tests/Networking/SystemPredictReconcileTest.cs +++ /dev/null @@ -1,473 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Robust.Client.GameStates; -using Robust.Client.Timing; -using Robust.Server.Player; -using Robust.Shared; -using Robust.Shared.Analyzers; -using Robust.Shared.Configuration; -using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; -using Robust.Shared.IoC; -using Robust.Shared.Map; -using Robust.Shared.Serialization; -using Robust.Shared.Timing; - -namespace Content.IntegrationTests.Tests.Networking -{ - // This test checks that the prediction & reconciling system is working correctly with a simple boolean flag. - // An entity system sets a flag on a networked component via a RaisePredictiveEvent, - // so it runs predicted on client and eventually on server. - // All the tick values are checked to ensure it arrives on client & server at the exact correct ticks. - // On the client, the reconciling system is checked to ensure that the state correctly reset every tick, - // until the server acknowledges it. - // Then, the same test is performed again, but the server does not handle the message (it ignores it). - // To simulate a mispredict. - // This means the client is forced to reset it once it gets to the server tick where the server didn't do anything. - // the tick where the server *should* have, but did not, acknowledge the state change. - // Finally, we run two events inside the prediction area to ensure reconciling does for incremental stuff. - [TestFixture] - public sealed class SystemPredictReconcileTest - { - [Test] - public async Task Test() - { - // TODO remove fresh=true. - // Instead, offset the all the explicit tick checks by some initial tick number. - await using var pairTracker = await PoolManager.GetServerClient(new() { Fresh = true, DummyTicker = true }); - var server = pairTracker.Pair.Server; - var client = pairTracker.Pair.Client; - - // Pull in all dependencies we need. - var sPlayerManager = server.ResolveDependency(); - var sMapManager = server.ResolveDependency(); - var sEntityManager = server.ResolveDependency(); - var cEntityManager = client.ResolveDependency(); - var sGameTiming = server.ResolveDependency(); - var cGameTiming = client.ResolveDependency(); - var cGameStateManager = client.ResolveDependency(); - var cfg = client.ResolveDependency(); - var log = cfg.GetCVar(CVars.NetLogging); - - //cfg.SetCVar(CVars.NetLogging, true); - - EntityUid serverEnt = default; - SystemPredictionTestComponent serverComponent = default!; - SystemPredictionTestComponent clientComponent = default!; - - var serverSystem = server.ResolveDependency() - .GetEntitySystem(); - var clientSystem = client.ResolveDependency() - .GetEntitySystem(); - - await server.WaitPost(() => - { - // Spawn dummy component entity. - var map = sMapManager.CreateMap(); - var player = sPlayerManager.ServerSessions.Single(); - serverEnt = sEntityManager.SpawnEntity(null, new MapCoordinates(new Vector2(0, 0), map)); - serverComponent = sEntityManager.AddComponent(serverEnt); - - // Make client "join game" so they receive game state updates. - player.JoinGame(); - }); - - // Run some ticks so that - await PoolManager.RunTicksSync(pairTracker.Pair, 3); - - // Check client buffer is full - Assert.That(cGameStateManager.CurrentBufferSize, Is.EqualTo(cGameStateManager.TargetBufferSize)); - - // This isn't required anymore, but the test had this for the sake of "technical things", and I cbf shifting - // all the tick times over. So it stays. - await client.WaitRunTicks(1); - - await client.WaitPost(() => - { - clientComponent = cEntityManager.GetComponent(serverEnt); - }); - - Assert.Multiple(() => - { - Assert.That(clientComponent.Foo, Is.False); - - // KEEP IN MIND WHEN READING THIS. - // The game loop increments CurTick AFTER running the tick. - // So when reading CurTick inside an Assert or Post or whatever, the tick reported is the NEXT one to run. - - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(14))); - Assert.That(serverComponent.Foo, Is.False); - - // Client last ran tick 15 meaning it's ahead of the last server tick it processed (12) - Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(16))); - Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick(12))); - }); - - // *** I am using block scopes to visually distinguish these sections of the test to make it more readable. - - - // Send an event to change the flag and instantly see the effect replicate client side, - // while it's queued on server and reconciling works (constantly needs re-firing on client). - { - await client.WaitPost(() => - { - cEntityManager.RaisePredictiveEvent(new SetFooMessage(serverEnt, true)); - - Assert.That(clientComponent.Foo, Is.True); - }); - - // Event correctly arrived on client system. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), true, false, true, true) })); - clientSystem.EventTriggerList.Clear(); - - // Two ticks happen on both sides with nothing really "changing". - // Server doesn't receive it yet, - // client is still replaying the past prediction. - for (var i = 0; i < 2; i++) - { - await server.WaitRunTicks(1); - - // Event did not arrive on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), false, false, true, true) })); - clientSystem.EventTriggerList.Clear(); - } - - { - await server.WaitRunTicks(1); - - Assert.Multiple(() => - { - // Event arrived on server at tick 16. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(17))); - Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), true, false, true, true) })); - }); - serverSystem.EventTriggerList.Clear(); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(16), false, false, true, true) })); - clientSystem.EventTriggerList.Clear(); - } - - { - await server.WaitRunTicks(1); - - // Nothing happened on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - Assert.Multiple(() => - { - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, Is.Empty); - Assert.That(clientComponent.Foo, Is.True); - }); - clientSystem.EventTriggerList.Clear(); - } - } - - // Disallow changes to simulate a misprediction. - serverSystem.Allow = false; - - Assert.Multiple(() => - { - // Assert timing is still correct, should be but it's a good reference for the rest of the test. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(18))); - Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(20))); - Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick(16))); - }); - - { - // Send event to server to change flag again, this time to disable it.. - await client.WaitPost(() => - { - cEntityManager.RaisePredictiveEvent(new SetFooMessage(serverEnt, false)); - - Assert.That(clientComponent.Foo, Is.False); - }); - - // Event correctly arrived on client system. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), true, true, false, false) })); - clientSystem.EventTriggerList.Clear(); - - for (var i = 0; i < 2; i++) - { - await server.WaitRunTicks(1); - - // Event did not arrive on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), false, true, false, false) })); - clientSystem.EventTriggerList.Clear(); - } - - { - await server.WaitRunTicks(1); - - Assert.Multiple(() => - { - // Event arrived on server at tick 20. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(21))); - // But the server didn't listen! - Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), true, true, true, false) })); - }); - serverSystem.EventTriggerList.Clear(); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(20), false, true, false, false) })); - clientSystem.EventTriggerList.Clear(); - } - - { - await server.WaitRunTicks(1); - - // Nothing happened on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - Assert.Multiple(() => - { - // Event no longer got repeated and flag was *not* set by server state. - // Mispredict gracefully handled! - Assert.That(clientSystem.EventTriggerList, Is.Empty); - Assert.That(clientComponent.Foo, Is.True); - }); - clientSystem.EventTriggerList.Clear(); - } - } - - // Re-allow changes to make everything work correctly again. - serverSystem.Allow = true; - - Assert.Multiple(() => - { - // Assert timing is still correct. - Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(22))); - Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(24))); - Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick(20))); - }); - - { - // Send first event to disable the flag (reminder: it never got accepted by the server). - await client.WaitPost(() => - { - cEntityManager.RaisePredictiveEvent(new SetFooMessage(serverEnt, false)); - - Assert.That(clientComponent.Foo, Is.False); - }); - - // Event correctly arrived on client system. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(24), true, true, false, false) })); - clientSystem.EventTriggerList.Clear(); - - // Run one tick, everything checks out. - { - await server.WaitRunTicks(1); - - // Event did not arrive on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(24), false, true, false, false) })); - clientSystem.EventTriggerList.Clear(); - } - - // Send another event, to re-enable it. - await client.WaitPost(() => - { - cEntityManager.RaisePredictiveEvent(new SetFooMessage(serverEnt, true)); - - Assert.That(clientComponent.Foo, Is.True); - }); - - // Event correctly arrived on client system. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(25), true, false, true, true) })); - clientSystem.EventTriggerList.Clear(); - - // Next tick we run, both events come in, but at different times. - { - await server.WaitRunTicks(1); - - // Event did not arrive on server. - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] - { - (new GameTick(24), false, true, false, false), (new GameTick(25), false, false, true, true) - })); - clientSystem.EventTriggerList.Clear(); - } - - // FIRST event arrives on server! - { - await server.WaitRunTicks(1); - - Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(24), true, true, false, false) })); - serverSystem.EventTriggerList.Clear(); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] - { - (new GameTick(24), false, true, false, false), (new GameTick(25), false, false, true, true) - })); - clientSystem.EventTriggerList.Clear(); - } - - // SECOND event arrived on server, client receives ack for first event, - // still runs second event as past prediction. - { - await server.WaitRunTicks(1); - - Assert.That(serverSystem.EventTriggerList, - Is.EquivalentTo(new[] { (new GameTick(25), true, false, true, true) })); - serverSystem.EventTriggerList.Clear(); - - await client.WaitRunTicks(1); - - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, - Is.EquivalentTo(new[] - { - (new GameTick(25), false, false, true, true) - })); - clientSystem.EventTriggerList.Clear(); - } - - // Finally, second event acknowledged on client and we're good. - { - await server.WaitRunTicks(1); - - Assert.That(serverSystem.EventTriggerList, Is.Empty); - - await client.WaitRunTicks(1); - - Assert.Multiple(() => - { - // Event got repeated on client as a past prediction. - Assert.That(clientSystem.EventTriggerList, Is.Empty); - - Assert.That(clientComponent.Foo, Is.True); - }); - } - } - - cfg.SetCVar(CVars.NetLogging, log); - await pairTracker.CleanReturnAsync(); - } - - [NetworkedComponent()] - [Access(typeof(SystemPredictionTestEntitySystem))] - [RegisterComponent] - public sealed class SystemPredictionTestComponent : Component - { - public bool Foo; - } - - public sealed class SystemPredictionTestEntitySystem : EntitySystem - { - public bool Allow { get; set; } = true; - - // Queue of all the events that come in so we can test that they come in perfectly as expected. - public List<(GameTick tick, bool firstPredict, bool old, bool @new, bool value)> EventTriggerList { get; } = - new(); - - [Dependency] private readonly IGameTiming _gameTiming = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeNetworkEvent(HandleMessage); - SubscribeLocalEvent(HandleMessage); - SubscribeLocalEvent(OnGetState); - SubscribeLocalEvent(OnHandleState); - } - - private void HandleMessage(SetFooMessage message, EntitySessionEventArgs args) - { - var component = EntityManager.GetComponent(message.Uid); - var old = component.Foo; - if (Allow) - { - component.Foo = message.NewFoo; - Dirty(message.Uid, component); - } - - EventTriggerList.Add((_gameTiming.CurTick, _gameTiming.IsFirstTimePredicted, old, component.Foo, message.NewFoo)); - } - - private void OnGetState(EntityUid uid, SystemPredictionTestComponent comp, ref ComponentGetState args) - { - args.State = new SystemPredictionComponentState(comp.Foo); - } - - private void OnHandleState(EntityUid uid, SystemPredictionTestComponent comp, ref ComponentHandleState args) - { - if (args.Current is not SystemPredictionComponentState state) - return; - - comp.Foo = state.Foo; - Dirty(uid, comp); - } - - [Serializable, NetSerializable] - private sealed class SystemPredictionComponentState : ComponentState - { - public bool Foo { get; } - - public SystemPredictionComponentState(bool foo) - { - Foo = foo; - } - } - } - - private sealed class SetFooMessage : EntityEventArgs - { - public SetFooMessage(EntityUid uid, bool newFoo) - { - Uid = uid; - NewFoo = newFoo; - } - - public EntityUid Uid { get; } - public bool NewFoo { get; } - } - } -} diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs index bdcaf0de4d..ecbf241de2 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs @@ -1,5 +1,4 @@ using System.Linq; -using Content.Server.Administration.Commands; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.Administration.UI; @@ -64,15 +63,20 @@ namespace Content.Server.Administration.Systems public override void Initialize() { - SubscribeLocalEvent>(AddAdminVerbs); - SubscribeLocalEvent>(AddDebugVerbs); - SubscribeLocalEvent>(AddSmiteVerbs); - SubscribeLocalEvent>(AddTricksVerbs); - SubscribeLocalEvent>(AddAntagVerbs); + SubscribeLocalEvent>(GetVerbs); SubscribeLocalEvent(Reset); SubscribeLocalEvent(OnSolutionChanged); } + private void GetVerbs(GetVerbsEvent ev) + { + AddAdminVerbs(ev); + AddDebugVerbs(ev); + AddSmiteVerbs(ev); + AddTricksVerbs(ev); + AddAntagVerbs(ev); + } + private void AddAdminVerbs(GetVerbsEvent args) { if (!EntityManager.TryGetComponent(args.User, out var actor)) diff --git a/Content.Server/Anomaly/AnomalySystem.Scanner.cs b/Content.Server/Anomaly/AnomalySystem.Scanner.cs index 0bac4de9f0..37c4384f2f 100644 --- a/Content.Server/Anomaly/AnomalySystem.Scanner.cs +++ b/Content.Server/Anomaly/AnomalySystem.Scanner.cs @@ -19,9 +19,7 @@ public sealed partial class AnomalySystem SubscribeLocalEvent(OnScannerAfterInteract); SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnScannerAnomalyShutdown); SubscribeLocalEvent(OnScannerAnomalySeverityChanged); - SubscribeLocalEvent(OnScannerAnomalyStabilityChanged); SubscribeLocalEvent(OnScannerAnomalyHealthChanged); } diff --git a/Content.Server/Anomaly/AnomalySystem.Vessel.cs b/Content.Server/Anomaly/AnomalySystem.Vessel.cs index 01fbccad21..893a968258 100644 --- a/Content.Server/Anomaly/AnomalySystem.Vessel.cs +++ b/Content.Server/Anomaly/AnomalySystem.Vessel.cs @@ -26,8 +26,20 @@ public sealed partial class AnomalySystem SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnVesselGetPointsPerSecond); SubscribeLocalEvent(OnUnpaused); - SubscribeLocalEvent(OnVesselAnomalyShutdown); - SubscribeLocalEvent(OnVesselAnomalyStabilityChanged); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnStabilityChanged); + } + + private void OnStabilityChanged(ref AnomalyStabilityChangedEvent args) + { + OnVesselAnomalyStabilityChanged(ref args); + OnScannerAnomalyStabilityChanged(ref args); + } + + private void OnShutdown(ref AnomalyShutdownEvent args) + { + OnVesselAnomalyShutdown(ref args); + OnScannerAnomalyShutdown(ref args); } private void OnExamined(EntityUid uid, AnomalyVesselComponent component, ExaminedEvent args) diff --git a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs index 0f49b8d944..6f98b67224 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs @@ -35,7 +35,6 @@ namespace Content.Server.Cargo.Systems SubscribeLocalEvent(OnApproveOrderMessage); SubscribeLocalEvent(OnOrderUIOpened); SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(Reset); Reset(); } @@ -45,11 +44,6 @@ namespace Content.Server.Cargo.Systems UpdateOrderState(uid, station); } - private void Reset(RoundRestartCleanupEvent ev) - { - Reset(); - } - private void Reset() { _timer = 0; diff --git a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs index c8864c2648..a49d0bddd8 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs @@ -369,6 +369,7 @@ public sealed partial class CargoSystem private void OnRoundRestart(RoundRestartCleanupEvent ev) { + Reset(); CleanupCargoShuttle(); if (_cfgManager.GetCVar(CCVars.GridFill))