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