From e0529b023d46e3a66b812124affad0fbdcd773ae Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 11 May 2026 14:20:46 +0200 Subject: [PATCH] test(vfx #C.1.5a): real-emitter verification in OnRemove test + unused using MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-review follow-up to 003c502: 1. Test 3 (OnRemove_StopsScriptsAndEmitters) now wires the runner into the real ParticleHookSink instead of a RecordingSink, registers a persistent EmitterDesc, lets the CreateParticleHook actually spawn an emitter, then asserts the sink killed it after OnRemove. Previously the test only verified runner-side state — sink.StopAllForEntity was never observably exercised, so a regression dropping that call would have passed silently. 2. Removed unused `using System.Numerics` from EntityScriptActivator.cs. No production code changes. Tests 1 and 2 unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Rendering/Vfx/EntityScriptActivator.cs | 1 - .../Vfx/EntityScriptActivatorTests.cs | 58 ++++++++++++++++--- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs b/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs index 7211f85..828ccae 100644 --- a/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs +++ b/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs @@ -1,5 +1,4 @@ using System; -using System.Numerics; using AcDream.Core.Vfx; using AcDream.Core.World; diff --git a/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs b/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs index d31ca29..d835e8c 100644 --- a/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs @@ -89,22 +89,64 @@ public sealed class EntityScriptActivatorTests Assert.Empty(p.Recording.Calls); } + /// + /// Persistent emitter: TotalDuration=0 and TotalParticles=0 prevent + /// auto-finish; InitialParticles=1 ensures a particle spawns at t=0 + /// without waiting for the Birthrate timer; Lifespan=999f keeps that + /// particle alive far past the test horizon. + /// + private static EmitterDesc BuildPersistentEmitterDesc() => new() + { + DatId = 100u, + Type = ParticleType.Still, + EmitterKind = ParticleEmitterKind.BirthratePerSec, + MaxParticles = 4, + InitialParticles = 1, + TotalParticles = 0, // 0 = no particle-count cap + TotalDuration = 0f, // 0 = no time-based finish + Lifespan = 999f, + LifetimeMin = 999f, + LifetimeMax = 999f, + Birthrate = 0.5f, + StartSize = 0.5f, + EndSize = 0.5f, + StartAlpha = 1f, + EndAlpha = 1f, + }; + [Fact] public void OnRemove_StopsScriptsAndEmitters() { - var p = BuildPipeline( - (0xAAu, BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100 })))); - var activator = new EntityScriptActivator(p.Runner, p.Sink, _ => 0xAAu); + // For this test we need the runner to dispatch into the REAL + // ParticleHookSink so OnRemove's sink.StopAllForEntity has a live + // emitter to kill. This is the only observable way to verify the + // call had effect without subclassing the sealed sink. + var registry = new EmitterDescRegistry(); + registry.Register(BuildPersistentEmitterDesc()); + + var system = new ParticleSystem(registry); + var hookSink = new ParticleHookSink(system); + + var script = BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100u, Offset = new Frame() })); + var table = new Dictionary { [0xAAu] = script }; + var runner = new PhysicsScriptRunner( + id => table.TryGetValue(id, out var s) ? s : null, + hookSink); // runner dispatches into real sink, not RecordingSink + + var activator = new EntityScriptActivator(runner, hookSink, _ => 0xAAu); var entity = MakeEntity(serverGuid: 0xCAFEu, position: Vector3.Zero); activator.OnCreate(entity); - Assert.Equal(1, p.Runner.ActiveScriptCount); + runner.Tick(0.001f); // fires the CreateParticleHook → spawns emitter + + Assert.True(system.ActiveEmitterCount > 0, + "Setup precondition failed: emitter should be alive after the hook fires."); activator.OnRemove(0xCAFEu); - Assert.Equal(0, p.Runner.ActiveScriptCount); - // Tick after Remove must not surface any further hook fires. - p.Runner.Tick(1.0f); - Assert.Empty(p.Recording.Calls); + Assert.Equal(0, runner.ActiveScriptCount); + // sink.StopAllForEntity marks the emitter Finished; system.Tick reaps it. + system.Tick(0.01f); + Assert.Equal(0, system.ActiveEmitterCount); } }