]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Fix embedded projectile deletion not being tracked by container (#36123)
authorTayrtahn <tayrtahn@gmail.com>
Fri, 28 Mar 2025 08:43:13 +0000 (04:43 -0400)
committerGitHub <noreply@github.com>
Fri, 28 Mar 2025 08:43:13 +0000 (11:43 +0300)
* Remove deleted projectiles from the container tracking them

* Gotta dirty the container

* Remove the container component when all embedded projectiles are gone

* Add test

* No clientside deletion of networked entities

* Move cleanup logic before deletion

Content.IntegrationTests/Tests/Embedding/EmbedTest.cs
Content.Shared/Projectiles/SharedProjectileSystem.cs

index 5e09b5c4820e93483db55e06b547d01c17643eaf..f9db064163691faa8ff7066359780cc821cdfa76 100644 (file)
@@ -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);
     }
+
+    /// <summary>
+    /// Throws two embeddable projectiles at a target, then deletes them
+    /// one at a time, making sure that they are tracked correctly and that
+    /// the <see cref="EmbeddedContainerComponent"/> is removed once all
+    /// projectiles are gone.
+    /// </summary>
+    [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<EmbeddableProjectileComponent>(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<EmbeddableProjectileComponent>(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<EmbeddedContainerComponent>(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<EmbeddedContainerComponent>();
+        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<EmbeddedContainerComponent>(ToServer(Target)),
+            "Target did not remove EmbeddedContainerComponent after both projectiles were deleted.");
+    }
 }
index d0cb3b22614f420754fabf2820eb4b744f4b0bbf..7161a39e0a280b8959a6809c68e02046126b0e41 100644 (file)
@@ -39,6 +39,7 @@ public abstract partial class SharedProjectileSystem : EntitySystem
         SubscribeLocalEvent<EmbeddableProjectileComponent, ThrowDoHitEvent>(OnEmbedThrowDoHit);
         SubscribeLocalEvent<EmbeddableProjectileComponent, ActivateInWorldEvent>(OnEmbedActivate);
         SubscribeLocalEvent<EmbeddableProjectileComponent, RemoveEmbeddedProjectileEvent>(OnEmbedRemove);
+        SubscribeLocalEvent<EmbeddableProjectileComponent, ComponentShutdown>(OnEmbeddableCompShutdown);
 
         SubscribeLocalEvent<EmbeddedContainerComponent, EntityTerminatingEvent>(OnEmbeddableTermination);
     }
@@ -75,6 +76,11 @@ public abstract partial class SharedProjectileSystem : EntitySystem
         _hands.TryPickupAnyHand(args.User, embeddable);
     }
 
+    private void OnEmbeddableCompShutdown(Entity<EmbeddableProjectileComponent> embeddable, ref ComponentShutdown arg)
+    {
+        EmbedDetach(embeddable, embeddable.Comp);
+    }
+
     private void OnEmbedThrowDoHit(Entity<EmbeddableProjectileComponent> 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<EmbeddedContainerComponent>(component.EmbeddedIntoUid.Value, out var embeddedContainer))
+            {
                 embeddedContainer.EmbeddedObjects.Remove(uid);
+                Dirty(component.EmbeddedIntoUid.Value, embeddedContainer);
+                if (embeddedContainer.EmbeddedObjects.Count == 0)
+                    RemCompDeferred<EmbeddedContainerComponent>(component.EmbeddedIntoUid.Value);
+            }
+        }
+
+        if (component.DeleteOnRemove && _net.IsServer)
+        {
+            QueueDel(uid);
+            return;
         }
 
         var xform = Transform(uid);