diff --git a/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs b/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs index 5e09b5c482..f9db064163 100644 --- a/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs +++ b/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs @@ -1,5 +1,6 @@ using Content.IntegrationTests.Tests.Interaction; using Content.Shared.Projectiles; +using Robust.Shared.GameObjects; using Robust.Shared.Network; namespace Content.IntegrationTests.Tests.Embedding; @@ -88,4 +89,84 @@ public sealed class EmbedTest : InteractionTest AssertExists(projectile); await AssertEntityLookup(EmbeddableProtoId); } + + /// + /// Throws two embeddable projectiles at a target, then deletes them + /// one at a time, making sure that they are tracked correctly and that + /// the is removed once all + /// projectiles are gone. + /// + [Test] + public async Task TestDeleteWhileEmbedded() + { + // Spawn the target we're going to throw at + await SpawnTarget(TargetProtoId); + + // Give the player the embeddable to throw + var projectile1 = await PlaceInHands(EmbeddableProtoId); + Assert.That(TryComp(projectile1, out var embedComp), + $"{EmbeddableProtoId} does not have EmbeddableProjectileComponent."); + // Make sure the projectile isn't already embedded into anything + Assert.That(embedComp.EmbeddedIntoUid, Is.Null, + $"Projectile already embedded into {SEntMan.ToPrettyString(embedComp.EmbeddedIntoUid)}."); + + // Have the player throw the embeddable at the target + await ThrowItem(); + + // Give the player a second embeddable to throw + var projectile2 = await PlaceInHands(EmbeddableProtoId); + Assert.That(TryComp(projectile1, out var embedComp2), + $"{EmbeddableProtoId} does not have EmbeddableProjectileComponent."); + + // Wait a moment for the projectile to hit and embed + await RunSeconds(0.5f); + + // Make sure the projectile is embedded into the target + Assert.That(embedComp.EmbeddedIntoUid, Is.EqualTo(ToServer(Target)), + "First projectile not embedded into target."); + Assert.That(TryComp(out var containerComp), + "Target was not given EmbeddedContainerComponent."); + Assert.That(containerComp.EmbeddedObjects, Does.Contain(ToServer(projectile1)), + "Target is not tracking the first projectile as embedded."); + Assert.That(containerComp.EmbeddedObjects, Has.Count.EqualTo(1), + "Target has unexpected EmbeddedObjects count."); + + // Wait for the cooldown between throws + await RunSeconds(Hands.ThrowCooldown.Seconds); + + // Throw the second projectile + await ThrowItem(); + + // Wait a moment for the second projectile to hit and embed + await RunSeconds(0.5f); + + Assert.That(embedComp2.EmbeddedIntoUid, Is.EqualTo(ToServer(Target)), + "Second projectile not embedded into target"); + AssertComp(); + Assert.That(containerComp.EmbeddedObjects, Does.Contain(ToServer(projectile1)), + "Target is not tracking the second projectile as embedded."); + Assert.That(containerComp.EmbeddedObjects, Has.Count.EqualTo(2), + "Target EmbeddedObjects count did not increase with second projectile."); + + // Delete the first projectile + await Delete(projectile1); + + Assert.That(containerComp.EmbeddedObjects, Does.Not.Contain(ToServer(projectile1)), + "Target did not stop tracking first projectile after it was deleted."); + Assert.That(containerComp.EmbeddedObjects, Does.Not.Contain(EntityUid.Invalid), + "Target EmbeddedObjects contains an invalid entity."); + foreach (var embedded in containerComp.EmbeddedObjects) + { + Assert.That(!SEntMan.Deleted(embedded), + "Target EmbeddedObjects contains a deleted entity."); + } + Assert.That(containerComp.EmbeddedObjects, Has.Count.EqualTo(1), + "Target EmbeddedObjects count did not decrease after deleting first projectile."); + + // Delete the second projectile + await Delete(projectile2); + + Assert.That(!SEntMan.HasComponent(ToServer(Target)), + "Target did not remove EmbeddedContainerComponent after both projectiles were deleted."); + } } diff --git a/Content.Shared/Projectiles/SharedProjectileSystem.cs b/Content.Shared/Projectiles/SharedProjectileSystem.cs index d0cb3b2261..7161a39e0a 100644 --- a/Content.Shared/Projectiles/SharedProjectileSystem.cs +++ b/Content.Shared/Projectiles/SharedProjectileSystem.cs @@ -39,6 +39,7 @@ public abstract partial class SharedProjectileSystem : EntitySystem SubscribeLocalEvent(OnEmbedThrowDoHit); SubscribeLocalEvent(OnEmbedActivate); SubscribeLocalEvent(OnEmbedRemove); + SubscribeLocalEvent(OnEmbeddableCompShutdown); SubscribeLocalEvent(OnEmbeddableTermination); } @@ -75,6 +76,11 @@ public abstract partial class SharedProjectileSystem : EntitySystem _hands.TryPickupAnyHand(args.User, embeddable); } + private void OnEmbeddableCompShutdown(Entity embeddable, ref ComponentShutdown arg) + { + EmbedDetach(embeddable, embeddable.Comp); + } + private void OnEmbedThrowDoHit(Entity embeddable, ref ThrowDoHitEvent args) { if (!embeddable.Comp.EmbedOnThrow) @@ -130,16 +136,21 @@ public abstract partial class SharedProjectileSystem : EntitySystem if (!Resolve(uid, ref component)) return; - if (component.DeleteOnRemove) - { - QueueDel(uid); - return; - } - if (component.EmbeddedIntoUid is not null) { if (TryComp(component.EmbeddedIntoUid.Value, out var embeddedContainer)) + { embeddedContainer.EmbeddedObjects.Remove(uid); + Dirty(component.EmbeddedIntoUid.Value, embeddedContainer); + if (embeddedContainer.EmbeddedObjects.Count == 0) + RemCompDeferred(component.EmbeddedIntoUid.Value); + } + } + + if (component.DeleteOnRemove && _net.IsServer) + { + QueueDel(uid); + return; } var xform = Transform(uid);