Prevent infinite loops in device linking (#16856)
This commit is contained in:
@@ -91,7 +91,7 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork
|
|||||||
deviceNetSystem.QueuePacket(device1, networkComponent2.Address, payload, networkComponent2.ReceiveFrequency.Value);
|
deviceNetSystem.QueuePacket(device1, networkComponent2.Address, payload, networkComponent2.ReceiveFrequency.Value);
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.WaitRunTicks(1);
|
await server.WaitRunTicks(2);
|
||||||
await server.WaitIdleAsync();
|
await server.WaitIdleAsync();
|
||||||
|
|
||||||
await server.WaitAssertion(() => {
|
await server.WaitAssertion(() => {
|
||||||
@@ -146,7 +146,7 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork
|
|||||||
deviceNetSystem.QueuePacket(device1, networkComponent2.Address, payload, networkComponent2.ReceiveFrequency.Value);
|
deviceNetSystem.QueuePacket(device1, networkComponent2.Address, payload, networkComponent2.ReceiveFrequency.Value);
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.WaitRunTicks(1);
|
await server.WaitRunTicks(2);
|
||||||
await server.WaitIdleAsync();
|
await server.WaitIdleAsync();
|
||||||
|
|
||||||
await server.WaitAssertion(() => {
|
await server.WaitAssertion(() => {
|
||||||
@@ -200,7 +200,7 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork
|
|||||||
["testbool"] = true
|
["testbool"] = true
|
||||||
};
|
};
|
||||||
|
|
||||||
await server.WaitRunTicks(1);
|
await server.WaitRunTicks(2);
|
||||||
await server.WaitIdleAsync();
|
await server.WaitIdleAsync();
|
||||||
|
|
||||||
await server.WaitAssertion(() => {
|
await server.WaitAssertion(() => {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using Content.Server.DeviceLinking.Systems;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
|
||||||
|
namespace Content.Server.DeviceLinking.Components.Overload;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="Content.Server.DeviceLinking.Events.DeviceLinkOverloadedEvent"/>
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
[Access(typeof(DeviceLinkOverloadSystem))]
|
||||||
|
public sealed class SoundOnOverloadComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sound to play when the device overloads
|
||||||
|
/// </summary>
|
||||||
|
[DataField("sound")]
|
||||||
|
public SoundSpecifier? OverloadSound = new SoundPathSpecifier("/Audio/Items/Defib/defib_zap.ogg");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modifies the volume the sound is played at
|
||||||
|
/// </summary>
|
||||||
|
[DataField("volumeModifier")]
|
||||||
|
public float VolumeModifier;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="Content.Server.DeviceLinking.Events.DeviceLinkOverloadedEvent"/>
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
[Access(typeof(DeviceLinkOverloadSystem))]
|
||||||
|
public sealed class SpawnOnOverloadComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The entity prototype to spawn when the device overloads
|
||||||
|
/// </summary>
|
||||||
|
[DataField("spawnedPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
|
public string Prototype = "PuddleSparkle";
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace Content.Server.DeviceLinking.Events;
|
||||||
|
|
||||||
|
[ByRefEvent]
|
||||||
|
public readonly record struct DeviceLinkOverloadedEvent;
|
||||||
@@ -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<SoundOnOverloadComponent, DeviceLinkOverloadedEvent>(OnOverloadSound);
|
||||||
|
SubscribeLocalEvent<SpawnOnOverloadComponent, DeviceLinkOverloadedEvent>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,23 @@ public sealed class DeviceLinkSystem : SharedDeviceLinkSystem
|
|||||||
SubscribeLocalEvent<DeviceLinkSinkComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
|
SubscribeLocalEvent<DeviceLinkSinkComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
var query = EntityQueryEnumerator<DeviceLinkSinkComponent>();
|
||||||
|
|
||||||
|
while (query.MoveNext(out var component))
|
||||||
|
{
|
||||||
|
if (component.InvokeLimit < 1)
|
||||||
|
{
|
||||||
|
component.InvokeCounter = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(component.InvokeCounter > 0)
|
||||||
|
component.InvokeCounter--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Moves existing links from machine linking to device linking to ensure linked things still work even when the map wasn't updated yet
|
/// Moves existing links from machine linking to device linking to ensure linked things still work even when the map wasn't updated yet
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -62,11 +79,25 @@ public sealed class DeviceLinkSystem : SharedDeviceLinkSystem
|
|||||||
if (!sourceComponent.LinkedPorts.TryGetValue(sinkUid, out var links))
|
if (!sourceComponent.LinkedPorts.TryGetValue(sinkUid, out var links))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (!TryComp<DeviceLinkSinkComponent>(sinkUid, out var sinkComponent))
|
||||||
|
continue;
|
||||||
|
|
||||||
foreach (var (source, sink) in links)
|
foreach (var (source, sink) in links)
|
||||||
{
|
{
|
||||||
if (source != port)
|
if (source != port)
|
||||||
continue;
|
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
|
//Just skip using device networking if the source or the sink doesn't support it
|
||||||
if (!HasComp<DeviceNetworkComponent>(uid) || !TryComp<DeviceNetworkComponent?>(sinkUid, out var sinkNetworkComponent))
|
if (!HasComp<DeviceNetworkComponent>(uid) || !TryComp<DeviceNetworkComponent?>(sinkUid, out var sinkNetworkComponent))
|
||||||
{
|
{
|
||||||
@@ -109,6 +140,4 @@ public sealed class DeviceLinkSystem : SharedDeviceLinkSystem
|
|||||||
RaiseLocalEvent(uid, ref eventArgs);
|
RaiseLocalEvent(uid, ref eventArgs);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ namespace Content.Server.DeviceLinking.Systems
|
|||||||
{
|
{
|
||||||
if (state == SignalState.High || state == SignalState.Momentary)
|
if (state == SignalState.High || state == SignalState.Momentary)
|
||||||
{
|
{
|
||||||
if (door.State != DoorState.Open)
|
if (door.State == DoorState.Closed)
|
||||||
_doorSystem.TryOpen(uid, door);
|
_doorSystem.TryOpen(uid, door);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ namespace Content.Server.DeviceLinking.Systems
|
|||||||
{
|
{
|
||||||
if (state == SignalState.High || state == SignalState.Momentary)
|
if (state == SignalState.High || state == SignalState.Momentary)
|
||||||
{
|
{
|
||||||
if (door.State != DoorState.Closed)
|
if (door.State == DoorState.Open)
|
||||||
_doorSystem.TryClose(uid, door);
|
_doorSystem.TryClose(uid, door);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +59,7 @@ namespace Content.Server.DeviceLinking.Systems
|
|||||||
{
|
{
|
||||||
if (state == SignalState.High || state == SignalState.Momentary)
|
if (state == SignalState.High || state == SignalState.Momentary)
|
||||||
{
|
{
|
||||||
|
if (door.State is DoorState.Closed or DoorState.Open)
|
||||||
_doorSystem.TryToggleDoor(uid, door);
|
_doorSystem.TryToggleDoor(uid, door);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ using Content.Shared.DeviceNetwork;
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Utility;
|
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Content.Shared.Examine;
|
using Content.Shared.Examine;
|
||||||
using static Content.Server.DeviceNetwork.Components.DeviceNetworkComponent;
|
|
||||||
|
|
||||||
namespace Content.Server.DeviceNetwork.Systems
|
namespace Content.Server.DeviceNetwork.Systems
|
||||||
{
|
{
|
||||||
@@ -23,21 +21,39 @@ namespace Content.Server.DeviceNetwork.Systems
|
|||||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||||
|
|
||||||
private readonly Dictionary<int, DeviceNet> _networks = new(4);
|
private readonly Dictionary<int, DeviceNet> _networks = new(4);
|
||||||
private readonly Queue<DeviceNetworkPacketEvent> _packets = new();
|
private readonly Queue<DeviceNetworkPacketEvent> _queueA = new();
|
||||||
|
private readonly Queue<DeviceNetworkPacketEvent> _queueB = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The queue being processed in the current tick
|
||||||
|
/// </summary>
|
||||||
|
private Queue<DeviceNetworkPacketEvent> _activeQueue = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The queue that will be processed in the next tick
|
||||||
|
/// </summary>
|
||||||
|
private Queue<DeviceNetworkPacketEvent> _nextQueue = null!;
|
||||||
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
SubscribeLocalEvent<DeviceNetworkComponent, MapInitEvent>(OnMapInit);
|
SubscribeLocalEvent<DeviceNetworkComponent, MapInitEvent>(OnMapInit);
|
||||||
SubscribeLocalEvent<DeviceNetworkComponent, ComponentShutdown>(OnNetworkShutdown);
|
SubscribeLocalEvent<DeviceNetworkComponent, ComponentShutdown>(OnNetworkShutdown);
|
||||||
SubscribeLocalEvent<DeviceNetworkComponent, ExaminedEvent>(OnExamine);
|
SubscribeLocalEvent<DeviceNetworkComponent, ExaminedEvent>(OnExamine);
|
||||||
|
|
||||||
|
_activeQueue = _queueA;
|
||||||
|
_nextQueue = _queueB;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
public override void Update(float frameTime)
|
||||||
{
|
{
|
||||||
while (_packets.TryDequeue(out var packet))
|
|
||||||
|
while (_activeQueue.TryDequeue(out var packet))
|
||||||
{
|
{
|
||||||
SendPacket(packet);
|
SendPacket(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwapQueues();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -62,10 +78,23 @@ namespace Content.Server.DeviceNetwork.Systems
|
|||||||
if (frequency == null)
|
if (frequency == null)
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Swaps the active queue.
|
||||||
|
/// Queues are swapped so that packets being sent in the current tick get processed in the next tick.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This prevents infinite loops while sending packets
|
||||||
|
/// </remarks>
|
||||||
|
private void SwapQueues()
|
||||||
|
{
|
||||||
|
_nextQueue = _activeQueue;
|
||||||
|
_activeQueue = _activeQueue == _queueA ? _queueB : _queueA;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnExamine(EntityUid uid, DeviceNetworkComponent device, ExaminedEvent args)
|
private void OnExamine(EntityUid uid, DeviceNetworkComponent device, ExaminedEvent args)
|
||||||
{
|
{
|
||||||
if (device.ExaminableAddress)
|
if (device.ExaminableAddress)
|
||||||
|
|||||||
@@ -19,4 +19,20 @@ public sealed class DeviceLinkSinkComponent : Component
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("links")]
|
[DataField("links")]
|
||||||
public HashSet<EntityUid> LinkedSources = new();
|
public HashSet<EntityUid> LinkedSources = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
[DataField("invokeCounter")]
|
||||||
|
public int InvokeCounter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("invokeLimit")]
|
||||||
|
public int InvokeLimit = 10;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,6 +315,20 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
|||||||
sinkComponent.LinkedSources.Add(sourceUid);
|
sinkComponent.LinkedSources.Add(sourceUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes every link from the given sink
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes all links between a source and a sink
|
/// Removes all links between a source and a sink
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -332,7 +346,7 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
|||||||
|
|
||||||
if (sourceComponent == null && sinkComponent == null)
|
if (sourceComponent == null && sinkComponent == null)
|
||||||
{
|
{
|
||||||
// Both were delted?
|
// Both were deleted?
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,8 @@
|
|||||||
- type: DeviceLinkSource
|
- type: DeviceLinkSource
|
||||||
ports:
|
ports:
|
||||||
- DoorStatus
|
- DoorStatus
|
||||||
|
- type: SoundOnOverload
|
||||||
|
- type: SpawnOnOverload
|
||||||
- type: UserInterface
|
- type: UserInterface
|
||||||
interfaces:
|
interfaces:
|
||||||
- key: enum.WiresUiKey.Key
|
- key: enum.WiresUiKey.Key
|
||||||
|
|||||||
Reference in New Issue
Block a user