]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Defibs will now also shock anyone still interacting with the target. (#35998)
authorCiarán Walsh <github@ciaranwal.sh>
Sat, 22 Nov 2025 23:44:26 +0000 (23:44 +0000)
committerGitHub <noreply@github.com>
Sat, 22 Nov 2025 23:44:26 +0000 (23:44 +0000)
* Defibs will now also shock anyone still interacting with the target.

* Improvements to test readability

* Apply fixes to other tests

* Refactor the interacting entities query to use an event.

* Include pullers as interacting with the entity they are pulling

* Broadcast event

* Use a constant

* Convert new test to InteractionTest

* Convert existing test

* Add behaviour note

* Revert "Convert existing test"

This reverts commit b8a8f2f68e3733bdb6ec254faf955a42096d47d7.

* Move new test into separate (InteractionTest) test file

* Use ToServer

* Use a constant for prototype id

* Use ToServer

* Add EntProtoId constructor

* Add assertion failure messages

* Manual cleanup of test entities

* Remove obsolete flag

* Add test summaries

* Remove tuple constructor

* Wrap entity deletion in WaitPost

* Extend DoAfter interacting test with an extra mob

Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs
Content.IntegrationTests/Tests/Puller/InteractingEntitiesTest.cs [new file with mode: 0644]
Content.Server/Medical/DefibrillatorSystem.cs
Content.Shared/DoAfter/SharedDoAfterSystem.cs
Content.Shared/Interaction/SharedInteractionSystem.cs
Content.Shared/Movement/Pulling/Systems/PullingSystem.cs

index 45c384f86c7091ab40210b5cec6eaeea6c24aa15..32f8b58542b4ff14f5d6b74643bb79976fe39c8a 100644 (file)
@@ -1,4 +1,6 @@
+using System.Collections.Generic;
 using Content.Shared.DoAfter;
+using Content.Shared.Interaction;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Map;
 using Robust.Shared.Reflection;
@@ -64,17 +66,16 @@ namespace Content.IntegrationTests.Tests.DoAfter
             var server = pair.Server;
             await server.WaitIdleAsync();
 
-            var entityManager = server.ResolveDependency<IEntityManager>();
+            var entityManager = server.EntMan;
             var timing = server.ResolveDependency<IGameTiming>();
-            var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
+            var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
             var ev = new TestDoAfterEvent();
 
             // That it finishes successfully
             await server.WaitPost(() =>
             {
-                var tickTime = 1.0f / timing.TickRate;
                 var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
-                var args = new DoAfterArgs(entityManager, mob, tickTime / 2, ev, null) { Broadcast = true };
+                var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod / 2, ev, null) { Broadcast = true };
 #pragma warning disable NUnit2045 // Interdependent assertions.
                 Assert.That(doAfterSystem.TryStartDoAfter(args));
                 Assert.That(ev.Cancelled, Is.False);
@@ -92,23 +93,17 @@ namespace Content.IntegrationTests.Tests.DoAfter
         {
             await using var pair = await PoolManager.GetServerClient();
             var server = pair.Server;
-            var entityManager = server.ResolveDependency<IEntityManager>();
+            var entityManager = server.EntMan;
             var timing = server.ResolveDependency<IGameTiming>();
-            var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
+            var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
             var ev = new TestDoAfterEvent();
 
             await server.WaitPost(() =>
             {
-                var tickTime = 1.0f / timing.TickRate;
-
                 var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
-                var args = new DoAfterArgs(entityManager, mob, tickTime * 2, ev, null) { Broadcast = true };
+                var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 2, ev, null) { Broadcast = true };
 
-                if (!doAfterSystem.TryStartDoAfter(args, out var id))
-                {
-                    Assert.Fail();
-                    return;
-                }
+                Assert.That(doAfterSystem.TryStartDoAfter(args, out var id));
 
                 Assert.That(!ev.Cancelled);
                 doAfterSystem.Cancel(id);
@@ -121,5 +116,67 @@ namespace Content.IntegrationTests.Tests.DoAfter
 
             await pair.CleanReturnAsync();
         }
+
+        /// <summary>
+        /// Spawns two sets of mobs with a targeted DoAfter to check that the GetEntitiesInteractingWithTarget result
+        /// includes the correct interacting entities.
+        /// </summary>
+        [Test]
+        public async Task TestGetInteractingEntities()
+        {
+            await using var pair = await PoolManager.GetServerClient();
+            var server = pair.Server;
+            var entityManager = server.EntMan;
+            var timing = server.ResolveDependency<IGameTiming>();
+            var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
+            var interactionSystem = entityManager.System<SharedInteractionSystem>();
+            var ev = new TestDoAfterEvent();
+
+            EntityUid mob = default;
+            EntityUid target = default;
+
+            EntityUid mob2 = default;
+            EntityUid mob3 = default;
+            EntityUid target2 = default;
+
+            await server.WaitPost(() =>
+            {
+                // Spawn two targets to interact with
+                target = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
+                target2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
+
+                // Spawn a mob which is interacting with the first target
+                mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
+                var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 5, ev, null, target) { Broadcast = true };
+                Assert.That(doAfterSystem.TryStartDoAfter(args));
+
+                // Spawn two more mobs which are interacting with the second target
+                mob2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
+                var args2 = new DoAfterArgs(entityManager, mob2, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
+                Assert.That(doAfterSystem.TryStartDoAfter(args2));
+
+                mob3 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
+                var args3 = new DoAfterArgs(entityManager, mob3, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
+                Assert.That(doAfterSystem.TryStartDoAfter(args3));
+            });
+
+            var list = new HashSet<EntityUid>();
+            interactionSystem.GetEntitiesInteractingWithTarget(target, list);
+            Assert.That(list, Is.EquivalentTo([mob]), $"{mob} was not considered to be interacting with {target}");
+
+            interactionSystem.GetEntitiesInteractingWithTarget(target2, list);
+            Assert.That(list, Is.EquivalentTo([mob2, mob3]), $"{mob2} and {mob3} were not considered to be interacting with {target2}");
+
+            await server.WaitPost(() =>
+            {
+                entityManager.DeleteEntity(mob);
+                entityManager.DeleteEntity(mob2);
+                entityManager.DeleteEntity(mob3);
+                entityManager.DeleteEntity(target);
+                entityManager.DeleteEntity(target2);
+            });
+
+            await pair.CleanReturnAsync();
+        }
     }
 }
index 37526f39a778f600901d13b5dd65b095555e81e0..6823e1ac97f2f32ce065cbb7dfb9e2eeeb4e0fe0 100644 (file)
@@ -49,6 +49,9 @@ public abstract partial class InteractionTest
         public static implicit operator EntitySpecifier(string prototype)
             => new(prototype, 1);
 
+        public static implicit operator EntitySpecifier(EntProtoId prototype)
+            => new(prototype.Id, 1);
+
         public static implicit operator EntitySpecifier((string, int) tuple)
             => new(tuple.Item1, tuple.Item2);
 
diff --git a/Content.IntegrationTests/Tests/Puller/InteractingEntitiesTest.cs b/Content.IntegrationTests/Tests/Puller/InteractingEntitiesTest.cs
new file mode 100644 (file)
index 0000000..b53a9a1
--- /dev/null
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Interaction;
+using Content.Shared.Movement.Pulling.Systems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Puller;
+
+#nullable enable
+
+public sealed class InteractingEntitiesTest : InteractionTest
+{
+    private static readonly EntProtoId MobHuman = "MobHuman";
+
+    /// <summary>
+    /// Spawns a Target mob, and a second mob which drags it,
+    /// and checks that the dragger is considered to be interacting with the dragged mob.
+    /// </summary>
+    [Test]
+    public async Task PullerIsConsideredInteractingTest()
+    {
+        await SpawnTarget(MobHuman);
+        var puller = await SpawnEntity(MobHuman, ToServer(TargetCoords));
+
+        var pullSys = SEntMan.System<PullingSystem>();
+        await Server.WaitAssertion(() =>
+        {
+            Assert.That(pullSys.TryStartPull(puller, ToServer(Target.Value)),
+                $"{puller} failed to start pulling {Target}");
+        });
+
+        var list = new HashSet<EntityUid>();
+        Server.System<SharedInteractionSystem>()
+            .GetEntitiesInteractingWithTarget(ToServer(Target.Value), list);
+        Assert.That(list, Is.EquivalentTo([puller]), $"{puller} was not considered to be interacting with {Target}");
+    }
+}
index f0dfceb14e1e6542fca555bc08dc3d89e4dee8ae..14ee155d2a4b5b249c4c7d23d593f5920faebdde 100644 (file)
@@ -18,6 +18,8 @@ using Content.Shared.Mind;
 using Content.Shared.Mobs;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
+using Content.Shared.Movement.Pulling.Components;
+using Content.Shared.PowerCell;
 using Content.Shared.Timing;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Player;
@@ -44,6 +46,7 @@ public sealed class DefibrillatorSystem : EntitySystem
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly SharedMindSystem _mind = default!;
     [Dependency] private readonly UseDelaySystem _useDelay = default!;
+    [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
 
     /// <inheritdoc/>
     public override void Initialize()
@@ -179,6 +182,18 @@ public sealed class DefibrillatorSystem : EntitySystem
 
         _audio.PlayPvs(component.ZapSound, uid);
         _electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true);
+
+        var interacters = new HashSet<EntityUid>();
+        _interactionSystem.GetEntitiesInteractingWithTarget(target, interacters);
+        foreach (var other in interacters)
+        {
+            if (other == user)
+                continue;
+
+            // Anyone else still operating on the target gets zapped too
+            _electrocution.TryDoElectrocution(other, null, component.ZapDamage, component.WritheDuration, true);
+        }
+
         if (!TryComp<UseDelayComponent>(uid, out var useDelay))
             return;
         _useDelay.SetLength((uid, useDelay), component.ZapDelay, component.DelayId);
index d80f65755ef167a1ffb603d174dd2ab426c73010..0b72692ea056f15446c66e94432e100b968ff1b1 100644 (file)
@@ -4,6 +4,7 @@ using Content.Shared.ActionBlocker;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Systems;
 using Content.Shared.Hands.Components;
+using Content.Shared.Interaction;
 using Content.Shared.Tag;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
@@ -35,6 +36,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
         SubscribeLocalEvent<DoAfterComponent, EntityUnpausedEvent>(OnUnpaused);
         SubscribeLocalEvent<DoAfterComponent, ComponentGetState>(OnDoAfterGetState);
         SubscribeLocalEvent<DoAfterComponent, ComponentHandleState>(OnDoAfterHandleState);
+        SubscribeLocalEvent<GetInteractingEntitiesEvent>(OnGetInteractingEntities);
     }
 
     private void OnUnpaused(EntityUid uid, DoAfterComponent component, ref EntityUnpausedEvent args)
@@ -131,6 +133,25 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
             EnsureComp<ActiveDoAfterComponent>(uid);
     }
 
+    /// <summary>
+    /// Adds entities which have an active DoAfter matching the target.
+    /// </summary>
+    private void OnGetInteractingEntities(ref GetInteractingEntitiesEvent args)
+    {
+        var enumerator = EntityQueryEnumerator<ActiveDoAfterComponent, DoAfterComponent>();
+        while (enumerator.MoveNext(out _, out var comp))
+        {
+            foreach (var doAfter in comp.DoAfters.Values)
+            {
+                if (doAfter.Cancelled || doAfter.Completed)
+                    continue;
+
+                if (doAfter.Args.Target == args.Target)
+                    args.InteractingEntities.Add(doAfter.Args.User);
+            }
+        }
+    }
+
     #region Creation
     /// <summary>
     ///     Tasks that are delayed until the specified time has passed
index c1bb855f3609361f5e5ab8bdfbfdd310464ac639..b4ee94c3aabd5b611ef6c2ab3f0881a40292025e 100644 (file)
@@ -1465,6 +1465,18 @@ namespace Content.Shared.Interaction
             return ev.Handled;
         }
 
+        /// <summary>
+        /// Get a list of entities which are currently considered to be interacting with the specified target entity.
+        /// Note: the result set is cleared on call.
+        /// </summary>
+        public void GetEntitiesInteractingWithTarget(EntityUid target, HashSet<EntityUid> result)
+        {
+            result.Clear();
+
+            var ev = new GetInteractingEntitiesEvent(target, result);
+            RaiseLocalEvent(target, ref ev, true);
+        }
+
         [Obsolete("Use ActionBlockerSystem")]
         public bool SupportsComplexInteractions(EntityUid user)
         {
@@ -1542,4 +1554,14 @@ namespace Content.Shared.Interaction
         public bool Handled;
         public bool InRange = false;
     }
+
+    /// <summary>
+    /// Raised to allow systems to provide entities which are interacting with the target entity.
+    /// </summary>
+    [ByRefEvent]
+    public record struct GetInteractingEntitiesEvent(EntityUid Target, HashSet<EntityUid> InteractingEntities)
+    {
+        public readonly EntityUid Target = Target;
+        public HashSet<EntityUid> InteractingEntities = InteractingEntities;
+    }
 }
index 3784dc0402a8f648df3e73572c89efe605838459..01a65f852a52fa697ef5e46cec8e82fdedca3c21 100644 (file)
@@ -68,6 +68,7 @@ public sealed class PullingSystem : EntitySystem
         SubscribeLocalEvent<PullableComponent, EntGotInsertedIntoContainerMessage>(OnPullableContainerInsert);
         SubscribeLocalEvent<PullableComponent, ModifyUncuffDurationEvent>(OnModifyUncuffDuration);
         SubscribeLocalEvent<PullableComponent, StopBeingPulledAlertEvent>(OnStopBeingPulledAlert);
+        SubscribeLocalEvent<PullableComponent, GetInteractingEntitiesEvent>(OnGetInteractingEntities);
 
         SubscribeLocalEvent<PullerComponent, UpdateMobStateEvent>(OnStateChanged, after: [typeof(MobThresholdSystem)]);
         SubscribeLocalEvent<PullerComponent, AfterAutoHandleStateEvent>(OnAfterState);
@@ -161,6 +162,12 @@ public sealed class PullingSystem : EntitySystem
         StopPulling(ent, ent);
     }
 
+    private void OnGetInteractingEntities(Entity<PullableComponent> ent, ref GetInteractingEntitiesEvent args)
+    {
+        if (ent.Comp.Puller != null)
+            args.InteractingEntities.Add(ent.Comp.Puller.Value);
+    }
+
     private void OnAfterState(Entity<PullerComponent> ent, ref AfterAutoHandleStateEvent args)
     {
         if (ent.Comp.Pulling == null)