Fix embedded projectile deletion not being tracked by container (#36123)

* 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
This commit is contained in:
Tayrtahn
2025-03-28 04:43:13 -04:00
committed by GitHub
parent 92a3fd99ca
commit b9517fcbe8
2 changed files with 98 additions and 6 deletions

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

View 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);