Defibs will now also shock anyone still interacting with the target. (#35998)

* 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
This commit is contained in:
Ciarán Walsh
2025-11-22 23:44:26 +00:00
committed by GitHub
parent f38a322912
commit c2f4b5145d
7 changed files with 177 additions and 14 deletions

View File

@@ -1,4 +1,6 @@
using System.Collections.Generic;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Reflection; using Robust.Shared.Reflection;
@@ -64,17 +66,16 @@ namespace Content.IntegrationTests.Tests.DoAfter
var server = pair.Server; var server = pair.Server;
await server.WaitIdleAsync(); await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>(); var entityManager = server.EntMan;
var timing = server.ResolveDependency<IGameTiming>(); var timing = server.ResolveDependency<IGameTiming>();
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>(); var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
var ev = new TestDoAfterEvent(); var ev = new TestDoAfterEvent();
// That it finishes successfully // That it finishes successfully
await server.WaitPost(() => await server.WaitPost(() =>
{ {
var tickTime = 1.0f / timing.TickRate;
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace); 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. #pragma warning disable NUnit2045 // Interdependent assertions.
Assert.That(doAfterSystem.TryStartDoAfter(args)); Assert.That(doAfterSystem.TryStartDoAfter(args));
Assert.That(ev.Cancelled, Is.False); Assert.That(ev.Cancelled, Is.False);
@@ -92,23 +93,17 @@ namespace Content.IntegrationTests.Tests.DoAfter
{ {
await using var pair = await PoolManager.GetServerClient(); await using var pair = await PoolManager.GetServerClient();
var server = pair.Server; var server = pair.Server;
var entityManager = server.ResolveDependency<IEntityManager>(); var entityManager = server.EntMan;
var timing = server.ResolveDependency<IGameTiming>(); var timing = server.ResolveDependency<IGameTiming>();
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>(); var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
var ev = new TestDoAfterEvent(); var ev = new TestDoAfterEvent();
await server.WaitPost(() => await server.WaitPost(() =>
{ {
var tickTime = 1.0f / timing.TickRate;
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace); 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.That(doAfterSystem.TryStartDoAfter(args, out var id));
{
Assert.Fail();
return;
}
Assert.That(!ev.Cancelled); Assert.That(!ev.Cancelled);
doAfterSystem.Cancel(id); doAfterSystem.Cancel(id);
@@ -121,5 +116,67 @@ namespace Content.IntegrationTests.Tests.DoAfter
await pair.CleanReturnAsync(); 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();
}
} }
} }

View File

@@ -49,6 +49,9 @@ public abstract partial class InteractionTest
public static implicit operator EntitySpecifier(string prototype) public static implicit operator EntitySpecifier(string prototype)
=> new(prototype, 1); => new(prototype, 1);
public static implicit operator EntitySpecifier(EntProtoId prototype)
=> new(prototype.Id, 1);
public static implicit operator EntitySpecifier((string, int) tuple) public static implicit operator EntitySpecifier((string, int) tuple)
=> new(tuple.Item1, tuple.Item2); => new(tuple.Item1, tuple.Item2);

View File

@@ -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}");
}
}

View File

@@ -18,6 +18,8 @@ using Content.Shared.Mind;
using Content.Shared.Mobs; using Content.Shared.Mobs;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.PowerCell;
using Content.Shared.Timing; using Content.Shared.Timing;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Player; using Robust.Shared.Player;
@@ -44,6 +46,7 @@ public sealed class DefibrillatorSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!; [Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
/// <inheritdoc/> /// <inheritdoc/>
public override void Initialize() public override void Initialize()
@@ -179,6 +182,18 @@ public sealed class DefibrillatorSystem : EntitySystem
_audio.PlayPvs(component.ZapSound, uid); _audio.PlayPvs(component.ZapSound, uid);
_electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true); _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)) if (!TryComp<UseDelayComponent>(uid, out var useDelay))
return; return;
_useDelay.SetLength((uid, useDelay), component.ZapDelay, component.DelayId); _useDelay.SetLength((uid, useDelay), component.ZapDelay, component.DelayId);

View File

@@ -4,6 +4,7 @@ using Content.Shared.ActionBlocker;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Systems; using Content.Shared.Damage.Systems;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Tag; using Content.Shared.Tag;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -35,6 +36,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
SubscribeLocalEvent<DoAfterComponent, EntityUnpausedEvent>(OnUnpaused); SubscribeLocalEvent<DoAfterComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<DoAfterComponent, ComponentGetState>(OnDoAfterGetState); SubscribeLocalEvent<DoAfterComponent, ComponentGetState>(OnDoAfterGetState);
SubscribeLocalEvent<DoAfterComponent, ComponentHandleState>(OnDoAfterHandleState); SubscribeLocalEvent<DoAfterComponent, ComponentHandleState>(OnDoAfterHandleState);
SubscribeLocalEvent<GetInteractingEntitiesEvent>(OnGetInteractingEntities);
} }
private void OnUnpaused(EntityUid uid, DoAfterComponent component, ref EntityUnpausedEvent args) private void OnUnpaused(EntityUid uid, DoAfterComponent component, ref EntityUnpausedEvent args)
@@ -131,6 +133,25 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
EnsureComp<ActiveDoAfterComponent>(uid); 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 #region Creation
/// <summary> /// <summary>
/// Tasks that are delayed until the specified time has passed /// Tasks that are delayed until the specified time has passed

View File

@@ -1465,6 +1465,18 @@ namespace Content.Shared.Interaction
return ev.Handled; 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")] [Obsolete("Use ActionBlockerSystem")]
public bool SupportsComplexInteractions(EntityUid user) public bool SupportsComplexInteractions(EntityUid user)
{ {
@@ -1542,4 +1554,14 @@ namespace Content.Shared.Interaction
public bool Handled; public bool Handled;
public bool InRange = false; 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;
}
} }

View File

@@ -68,6 +68,7 @@ public sealed class PullingSystem : EntitySystem
SubscribeLocalEvent<PullableComponent, EntGotInsertedIntoContainerMessage>(OnPullableContainerInsert); SubscribeLocalEvent<PullableComponent, EntGotInsertedIntoContainerMessage>(OnPullableContainerInsert);
SubscribeLocalEvent<PullableComponent, ModifyUncuffDurationEvent>(OnModifyUncuffDuration); SubscribeLocalEvent<PullableComponent, ModifyUncuffDurationEvent>(OnModifyUncuffDuration);
SubscribeLocalEvent<PullableComponent, StopBeingPulledAlertEvent>(OnStopBeingPulledAlert); SubscribeLocalEvent<PullableComponent, StopBeingPulledAlertEvent>(OnStopBeingPulledAlert);
SubscribeLocalEvent<PullableComponent, GetInteractingEntitiesEvent>(OnGetInteractingEntities);
SubscribeLocalEvent<PullerComponent, UpdateMobStateEvent>(OnStateChanged, after: [typeof(MobThresholdSystem)]); SubscribeLocalEvent<PullerComponent, UpdateMobStateEvent>(OnStateChanged, after: [typeof(MobThresholdSystem)]);
SubscribeLocalEvent<PullerComponent, AfterAutoHandleStateEvent>(OnAfterState); SubscribeLocalEvent<PullerComponent, AfterAutoHandleStateEvent>(OnAfterState);
@@ -161,6 +162,12 @@ public sealed class PullingSystem : EntitySystem
StopPulling(ent, ent); 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) private void OnAfterState(Entity<PullerComponent> ent, ref AfterAutoHandleStateEvent args)
{ {
if (ent.Comp.Pulling == null) if (ent.Comp.Pulling == null)