New ~50-line orchestrator that fires Setup.DefaultScript through the already-shipped PhysicsScriptRunner on entity spawn and stops scripts + live emitters on despawn. Resolver delegate avoids DatCollection coupling so the class is fully unit-testable with stubs. Three xUnit tests cover the three branches: fire-with-script, no-op-without-script, stop-on-remove. No wiring into the live spawn path yet -- that lands in the next commit. Spec: docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
4 KiB
C#
110 lines
4 KiB
C#
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Vfx;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.Types;
|
|
using Xunit;
|
|
using DatPhysicsScript = DatReaderWriter.DBObjs.PhysicsScript;
|
|
|
|
namespace AcDream.Core.Tests.Rendering.Vfx;
|
|
|
|
public sealed class EntityScriptActivatorTests
|
|
{
|
|
/// <summary>Recording sink so we can assert which hooks the runner fires.</summary>
|
|
private sealed class RecordingSink : IAnimationHookSink
|
|
{
|
|
public List<(uint EntityId, Vector3 Pos, AnimationHook Hook)> Calls = new();
|
|
public void OnHook(uint entityId, Vector3 worldPos, AnimationHook hook)
|
|
=> Calls.Add((entityId, worldPos, hook));
|
|
}
|
|
|
|
private static DatPhysicsScript BuildScript(params (double time, AnimationHook hook)[] items)
|
|
{
|
|
var script = new DatPhysicsScript();
|
|
foreach (var (t, h) in items)
|
|
script.ScriptData.Add(new PhysicsScriptData { StartTime = t, Hook = h });
|
|
return script;
|
|
}
|
|
|
|
private static WorldEntity MakeEntity(uint serverGuid, Vector3 position) =>
|
|
new()
|
|
{
|
|
Id = serverGuid,
|
|
ServerGuid = serverGuid,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = position,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = System.Array.Empty<MeshRef>(),
|
|
};
|
|
|
|
private record Pipeline(
|
|
ParticleSystem System,
|
|
ParticleHookSink Sink,
|
|
PhysicsScriptRunner Runner,
|
|
RecordingSink Recording);
|
|
|
|
private static Pipeline BuildPipeline(params (uint id, DatPhysicsScript script)[] scripts)
|
|
{
|
|
var registry = new EmitterDescRegistry();
|
|
var system = new ParticleSystem(registry);
|
|
var hookSink = new ParticleHookSink(system); // for activator's StopAllForEntity
|
|
var recording = new RecordingSink(); // for runner's hook dispatch
|
|
var table = new Dictionary<uint, DatPhysicsScript>();
|
|
foreach (var (id, s) in scripts) table[id] = s;
|
|
var runner = new PhysicsScriptRunner(
|
|
id => table.TryGetValue(id, out var s) ? s : null,
|
|
recording);
|
|
return new Pipeline(system, hookSink, runner, recording);
|
|
}
|
|
|
|
[Fact]
|
|
public void OnCreate_WithDefaultScript_FiresRunnerWithEntityGuidAndPosition()
|
|
{
|
|
var p = BuildPipeline(
|
|
(0xAAu, BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100 }))));
|
|
var activator = new EntityScriptActivator(p.Runner, p.Sink, _ => 0xAAu);
|
|
var entity = MakeEntity(serverGuid: 0xCAFEu, position: new Vector3(1, 2, 3));
|
|
|
|
activator.OnCreate(entity);
|
|
|
|
Assert.Equal(1, p.Runner.ActiveScriptCount);
|
|
p.Runner.Tick(0.001f);
|
|
Assert.Single(p.Recording.Calls);
|
|
Assert.Equal(0xCAFEu, p.Recording.Calls[0].EntityId);
|
|
Assert.Equal(new Vector3(1, 2, 3), p.Recording.Calls[0].Pos);
|
|
}
|
|
|
|
[Fact]
|
|
public void OnCreate_WithoutDefaultScript_DoesNothing()
|
|
{
|
|
var p = BuildPipeline(); // no scripts registered
|
|
var activator = new EntityScriptActivator(p.Runner, p.Sink, _ => 0u);
|
|
var entity = MakeEntity(serverGuid: 0xCAFEu, position: Vector3.Zero);
|
|
|
|
activator.OnCreate(entity);
|
|
|
|
Assert.Equal(0, p.Runner.ActiveScriptCount);
|
|
Assert.Empty(p.Recording.Calls);
|
|
}
|
|
|
|
[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);
|
|
var entity = MakeEntity(serverGuid: 0xCAFEu, position: Vector3.Zero);
|
|
|
|
activator.OnCreate(entity);
|
|
Assert.Equal(1, p.Runner.ActiveScriptCount);
|
|
|
|
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);
|
|
}
|
|
}
|