diff --git a/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs b/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs index 4d8a741e73..ec62e71679 100644 --- a/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs +++ b/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs @@ -91,7 +91,7 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork deviceNetSystem.QueuePacket(device1, networkComponent2.Address, payload, networkComponent2.ReceiveFrequency.Value); }); - await server.WaitRunTicks(1); + await server.WaitRunTicks(2); await server.WaitIdleAsync(); await server.WaitAssertion(() => { @@ -146,7 +146,7 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork deviceNetSystem.QueuePacket(device1, networkComponent2.Address, payload, networkComponent2.ReceiveFrequency.Value); }); - await server.WaitRunTicks(1); + await server.WaitRunTicks(2); await server.WaitIdleAsync(); await server.WaitAssertion(() => { @@ -200,7 +200,7 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork ["testbool"] = true }; - await server.WaitRunTicks(1); + await server.WaitRunTicks(2); await server.WaitIdleAsync(); await server.WaitAssertion(() => { diff --git a/Content.Server/DeviceLinking/Components/Overload/SoundOnOverloadComponent.cs b/Content.Server/DeviceLinking/Components/Overload/SoundOnOverloadComponent.cs new file mode 100644 index 0000000000..b1ff146c83 --- /dev/null +++ b/Content.Server/DeviceLinking/Components/Overload/SoundOnOverloadComponent.cs @@ -0,0 +1,26 @@ +using Content.Server.DeviceLinking.Systems; +using Robust.Shared.Audio; + +namespace Content.Server.DeviceLinking.Components.Overload; + +/// +/// Plays a sound when a device link overloads. +/// An overload happens when a device link sink is invoked to many times per tick +/// and it raises a +/// +[RegisterComponent] +[Access(typeof(DeviceLinkOverloadSystem))] +public sealed class SoundOnOverloadComponent : Component +{ + /// + /// Sound to play when the device overloads + /// + [DataField("sound")] + public SoundSpecifier? OverloadSound = new SoundPathSpecifier("/Audio/Items/Defib/defib_zap.ogg"); + + /// + /// Modifies the volume the sound is played at + /// + [DataField("volumeModifier")] + public float VolumeModifier; +} diff --git a/Content.Server/DeviceLinking/Components/Overload/SpawnOnOverloadComponent.cs b/Content.Server/DeviceLinking/Components/Overload/SpawnOnOverloadComponent.cs new file mode 100644 index 0000000000..ece7c2b7de --- /dev/null +++ b/Content.Server/DeviceLinking/Components/Overload/SpawnOnOverloadComponent.cs @@ -0,0 +1,21 @@ +using Content.Server.DeviceLinking.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.DeviceLinking.Components.Overload; + +/// +/// Spawns an entity when a device link overloads. +/// An overload happens when a device link sink is invoked to many times per tick +/// and it raises a +/// +[RegisterComponent] +[Access(typeof(DeviceLinkOverloadSystem))] +public sealed class SpawnOnOverloadComponent : Component +{ + /// + /// The entity prototype to spawn when the device overloads + /// + [DataField("spawnedPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string Prototype = "PuddleSparkle"; +} diff --git a/Content.Server/DeviceLinking/Events/DeviceLinkOverloadedEvent.cs b/Content.Server/DeviceLinking/Events/DeviceLinkOverloadedEvent.cs new file mode 100644 index 0000000000..ef35603e88 --- /dev/null +++ b/Content.Server/DeviceLinking/Events/DeviceLinkOverloadedEvent.cs @@ -0,0 +1,4 @@ +namespace Content.Server.DeviceLinking.Events; + +[ByRefEvent] +public readonly record struct DeviceLinkOverloadedEvent; diff --git a/Content.Server/DeviceLinking/Systems/DeviceLinkOverloadSystem.cs b/Content.Server/DeviceLinking/Systems/DeviceLinkOverloadSystem.cs new file mode 100644 index 0000000000..8f4bade83a --- /dev/null +++ b/Content.Server/DeviceLinking/Systems/DeviceLinkOverloadSystem.cs @@ -0,0 +1,29 @@ +using Content.Server.DeviceLinking.Components; +using Content.Server.DeviceLinking.Components.Overload; +using Content.Server.DeviceLinking.Events; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; + +namespace Content.Server.DeviceLinking.Systems; + +public sealed class DeviceLinkOverloadSystem : EntitySystem +{ + [Dependency] private readonly AudioSystem _audioSystem = default!; + public override void Initialize() + { + SubscribeLocalEvent(OnOverloadSound); + SubscribeLocalEvent(OnOverloadSpawn); + } + + private void OnOverloadSound(EntityUid uid, SoundOnOverloadComponent component, ref DeviceLinkOverloadedEvent args) + { + + _audioSystem.PlayPvs(component.OverloadSound, uid, AudioParams.Default.WithVolume(component.VolumeModifier)); + } + + + private void OnOverloadSpawn(EntityUid uid, SpawnOnOverloadComponent component, ref DeviceLinkOverloadedEvent args) + { + Spawn(component.Prototype, Transform(uid).Coordinates); + } +} diff --git a/Content.Server/DeviceLinking/Systems/DeviceLinkSystem.cs b/Content.Server/DeviceLinking/Systems/DeviceLinkSystem.cs index 60c85fde7b..9a7da9033c 100644 --- a/Content.Server/DeviceLinking/Systems/DeviceLinkSystem.cs +++ b/Content.Server/DeviceLinking/Systems/DeviceLinkSystem.cs @@ -19,6 +19,23 @@ public sealed class DeviceLinkSystem : SharedDeviceLinkSystem SubscribeLocalEvent(OnPacketReceived); } + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var component)) + { + if (component.InvokeLimit < 1) + { + component.InvokeCounter = 0; + continue; + } + + if(component.InvokeCounter > 0) + component.InvokeCounter--; + } + } + /// /// Moves existing links from machine linking to device linking to ensure linked things still work even when the map wasn't updated yet /// @@ -62,11 +79,25 @@ public sealed class DeviceLinkSystem : SharedDeviceLinkSystem if (!sourceComponent.LinkedPorts.TryGetValue(sinkUid, out var links)) continue; + if (!TryComp(sinkUid, out var sinkComponent)) + continue; + foreach (var (source, sink) in links) { if (source != port) continue; + if (sinkComponent.InvokeCounter > sinkComponent.InvokeLimit) + { + sinkComponent.InvokeCounter = 0; + var args = new DeviceLinkOverloadedEvent(); + RaiseLocalEvent(sinkUid, ref args); + RemoveAllFromSink(sinkUid, sinkComponent); + continue; + } + + sinkComponent.InvokeCounter++; + //Just skip using device networking if the source or the sink doesn't support it if (!HasComp(uid) || !TryComp(sinkUid, out var sinkNetworkComponent)) { @@ -109,6 +140,4 @@ public sealed class DeviceLinkSystem : SharedDeviceLinkSystem RaiseLocalEvent(uid, ref eventArgs); } #endregion - - } diff --git a/Content.Server/DeviceLinking/Systems/DoorSignalControlSystem.cs b/Content.Server/DeviceLinking/Systems/DoorSignalControlSystem.cs index 90328ddf31..8b522c8d14 100644 --- a/Content.Server/DeviceLinking/Systems/DoorSignalControlSystem.cs +++ b/Content.Server/DeviceLinking/Systems/DoorSignalControlSystem.cs @@ -43,7 +43,7 @@ namespace Content.Server.DeviceLinking.Systems { if (state == SignalState.High || state == SignalState.Momentary) { - if (door.State != DoorState.Open) + if (door.State == DoorState.Closed) _doorSystem.TryOpen(uid, door); } } @@ -51,7 +51,7 @@ namespace Content.Server.DeviceLinking.Systems { if (state == SignalState.High || state == SignalState.Momentary) { - if (door.State != DoorState.Closed) + if (door.State == DoorState.Open) _doorSystem.TryClose(uid, door); } } @@ -59,7 +59,8 @@ namespace Content.Server.DeviceLinking.Systems { if (state == SignalState.High || state == SignalState.Momentary) { - _doorSystem.TryToggleDoor(uid, door); + if (door.State is DoorState.Closed or DoorState.Open) + _doorSystem.TryToggleDoor(uid, door); } } else if (args.Port == component.InBolt) diff --git a/Content.Server/DeviceNetwork/Systems/DeviceNetworkSystem.cs b/Content.Server/DeviceNetwork/Systems/DeviceNetworkSystem.cs index c03bbcd229..d67ae1a086 100644 --- a/Content.Server/DeviceNetwork/Systems/DeviceNetworkSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/DeviceNetworkSystem.cs @@ -3,11 +3,9 @@ using Content.Shared.DeviceNetwork; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Utility; using System.Buffers; using System.Diagnostics.CodeAnalysis; using Content.Shared.Examine; -using static Content.Server.DeviceNetwork.Components.DeviceNetworkComponent; namespace Content.Server.DeviceNetwork.Systems { @@ -23,21 +21,39 @@ namespace Content.Server.DeviceNetwork.Systems [Dependency] private readonly SharedTransformSystem _transformSystem = default!; private readonly Dictionary _networks = new(4); - private readonly Queue _packets = new(); + private readonly Queue _queueA = new(); + private readonly Queue _queueB = new(); + + /// + /// The queue being processed in the current tick + /// + private Queue _activeQueue = null!; + + /// + /// The queue that will be processed in the next tick + /// + private Queue _nextQueue = null!; + public override void Initialize() { SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnNetworkShutdown); SubscribeLocalEvent(OnExamine); + + _activeQueue = _queueA; + _nextQueue = _queueB; } public override void Update(float frameTime) { - while (_packets.TryDequeue(out var packet)) + + while (_activeQueue.TryDequeue(out var packet)) { SendPacket(packet); } + + SwapQueues(); } /// @@ -62,10 +78,23 @@ namespace Content.Server.DeviceNetwork.Systems if (frequency == null) return false; - _packets.Enqueue(new DeviceNetworkPacketEvent(device.DeviceNetId, address, frequency.Value, device.Address, uid, data)); + _nextQueue.Enqueue(new DeviceNetworkPacketEvent(device.DeviceNetId, address, frequency.Value, device.Address, uid, data)); return true; } + /// + /// Swaps the active queue. + /// Queues are swapped so that packets being sent in the current tick get processed in the next tick. + /// + /// + /// This prevents infinite loops while sending packets + /// + private void SwapQueues() + { + _nextQueue = _activeQueue; + _activeQueue = _activeQueue == _queueA ? _queueB : _queueA; + } + private void OnExamine(EntityUid uid, DeviceNetworkComponent device, ExaminedEvent args) { if (device.ExaminableAddress) diff --git a/Content.Shared/DeviceLinking/DeviceLinkSinkComponent.cs b/Content.Shared/DeviceLinking/DeviceLinkSinkComponent.cs index 3538b9474c..1bb0fc8971 100644 --- a/Content.Shared/DeviceLinking/DeviceLinkSinkComponent.cs +++ b/Content.Shared/DeviceLinking/DeviceLinkSinkComponent.cs @@ -19,4 +19,20 @@ public sealed class DeviceLinkSinkComponent : Component /// [DataField("links")] public HashSet LinkedSources = new(); + + /// + /// Counts the amount of times a sink has been invoked for severing the link if this counter gets to high + /// The counter is counted down by one every tick if it's higher than 0 + /// This is for preventing infinite loops + /// + [DataField("invokeCounter")] + public int InvokeCounter; + + /// + /// How high the invoke counter is allowed to get before the links to the sink are removed and the DeviceLinkOverloadedEvent gets raised + /// If the invoke limit is smaller than 1 the sink can't overload + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("invokeLimit")] + public int InvokeLimit = 10; } diff --git a/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs b/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs index 79de58dcc2..f2b0f57c66 100644 --- a/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs +++ b/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs @@ -315,6 +315,20 @@ public abstract class SharedDeviceLinkSystem : EntitySystem sinkComponent.LinkedSources.Add(sourceUid); } + /// + /// Removes every link from the given sink + /// + public void RemoveAllFromSink(EntityUid sinkUid, DeviceLinkSinkComponent? sinkComponent = null) + { + if (!Resolve(sinkUid, ref sinkComponent)) + return; + + foreach (var sourceUid in sinkComponent.LinkedSources) + { + RemoveSinkFromSource(sourceUid, sinkUid, null, sinkComponent); + } + } + /// /// Removes all links between a source and a sink /// @@ -332,7 +346,7 @@ public abstract class SharedDeviceLinkSystem : EntitySystem if (sourceComponent == null && sinkComponent == null) { - // Both were delted? + // Both were deleted? return; } diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml index c2d3d33df8..b314022a98 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml @@ -94,6 +94,8 @@ - type: DeviceLinkSource ports: - DoorStatus + - type: SoundOnOverload + - type: SpawnOnOverload - type: UserInterface interfaces: - key: enum.WiresUiKey.Key