docs(vfx #C.1.5a): implementation plan + spec wiring-location fixes

Plan: docs/superpowers/plans/2026-05-12-phase-c1.5a-portals.md. Four
tasks, TDD-style, each task is a single commit boundary:
  1. EntityScriptActivator class + three xUnit tests
  2. Wire into GpuWorldState (new optional ctor param + two ?. calls)
  3. Construct in GameWindow with resolver lambda
  4. Visual verification at Holtburg Town network portal + roadmap update

Spec amendments correct an inaccuracy in the 2026-05-12 commit
(06d7fbd): the activator's call sites live in GpuWorldState
(AppendLiveEntity / RemoveEntityByServerGuid), not directly in
GameWindow as the original spec described. Also fixes the test file
path: tests/AcDream.Core.Tests/... not AcDream.App.Tests/... per the
existing test-project convention. No design changes — same activator,
same trigger condition, same lifecycle ordering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-11 13:36:18 +02:00
parent 06d7fbd5ef
commit ed5335b81e
2 changed files with 694 additions and 20 deletions

View file

@ -25,9 +25,12 @@ swirl matches retail in color, density, motion, and persistence.
**In:**
- New class `EntityScriptActivator` (one file, ~50 lines).
- Wiring of activator's `OnCreate` / `OnRemove` calls into `GameWindow`'s
spawn-lifecycle handlers, immediately after the matching `EntitySpawnAdapter`
calls.
- Wiring of activator's `OnCreate` / `OnRemove` calls into `GpuWorldState`'s
spawn-lifecycle methods (`AppendLiveEntity` / `RemoveEntityByServerGuid`),
immediately after the matching `EntitySpawnAdapter` calls. The activator is
constructed in `GameWindow` (where `_dats`, `_scriptRunner`, and
`_particleSink` are in scope) and passed into `GpuWorldState`'s constructor
as a new optional parameter, paralleling how `EntitySpawnAdapter` is wired.
- Three unit tests covering the activator's three branches
(fire / no-op-on-zero / cleanup-on-remove).
- Visual verification at the Holtburg Town network portal.
@ -145,18 +148,32 @@ data faithfully and the visual is what retail shows. If retail does NOT show
those particles and we do, that's evidence of a different gate retail
applies — to be investigated when seen.
### Wiring point: GameWindow
### Wiring point: GpuWorldState
Live entity spawn / despawn already flows through
[`GpuWorldState`](../../../src/AcDream.App/Streaming/GpuWorldState.cs) on the
render thread — the network layer pushes spawns into
`AppendLiveEntity(landblockId, entity)`, the server's `RemoveObject` opcode
routes through `RemoveEntityByServerGuid(serverGuid)`. The existing
`EntitySpawnAdapter` lifecycle hooks live at those two call sites
(line 345 `OnCreate`, line 285 `OnRemove`). The activator hooks fire
immediately after, in the same order:
```csharp
// In GameWindow.OnCreateObject(WorldEntity entity):
_entitySpawnAdapter.OnCreate(entity);
_entityScriptActivator.OnCreate(entity); // NEW — runs after meshes are registered
// GpuWorldState.AppendLiveEntity (line ~345):
_wbEntitySpawnAdapter?.OnCreate(entity);
_entityScriptActivator?.OnCreate(entity); // NEW — fires DefaultScript
// In GameWindow.OnRemoveObject(uint serverGuid):
_entitySpawnAdapter.OnRemove(serverGuid);
_entityScriptActivator.OnRemove(serverGuid); // NEW — runs in same order as create
// GpuWorldState.RemoveEntityByServerGuid (line ~285):
_wbEntitySpawnAdapter?.OnRemove(serverGuid);
_entityScriptActivator?.OnRemove(serverGuid); // NEW — stops scripts + emitters
```
`GpuWorldState`'s constructor grows a fifth (optional) parameter for the
activator, paralleling how `EntitySpawnAdapter` is plumbed today. `GameWindow`
constructs the activator alongside `_wbEntitySpawnAdapter` and passes it
through.
Production resolver lambda, constructed in `GameWindow` where `_dats` is in
scope:
@ -183,9 +200,9 @@ The try/catch matches the pattern in
### On spawn
```
GameWindow.OnCreateObject(entity)
├─ _entitySpawnAdapter.OnCreate(entity) // meshes ref-counted, animation state built
└─ _entityScriptActivator.OnCreate(entity)
GpuWorldState.AppendLiveEntity(landblockId, entity)
├─ _wbEntitySpawnAdapter?.OnCreate(entity) // meshes ref-counted, animation state built
└─ _entityScriptActivator?.OnCreate(entity)
├─ scriptId = resolver(entity) // Setup.DefaultScript.DataId, or 0 on miss/throw
├─ if (scriptId == 0) return // no DefaultScript → no-op
└─ _scriptRunner.Play(scriptId,
@ -199,9 +216,9 @@ GameWindow.OnCreateObject(entity)
### On despawn
```
GameWindow.OnRemoveObject(serverGuid)
├─ _entitySpawnAdapter.OnRemove(serverGuid) // meshes ref-decremented, state cleared
└─ _entityScriptActivator.OnRemove(serverGuid)
GpuWorldState.RemoveEntityByServerGuid(serverGuid)
├─ _wbEntitySpawnAdapter?.OnRemove(serverGuid) // meshes ref-decremented, state cleared
└─ _entityScriptActivator?.OnRemove(serverGuid)
├─ _scriptRunner.StopAllForEntity(serverGuid) // drop pending hooks
└─ _particleSink.StopAllForEntity(serverGuid, false) // kill live emitters (no fade)
```
@ -270,7 +287,11 @@ new threading concerns introduced.
### Unit tests (slice 1's gating tests)
`tests/AcDream.App.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs`. Uses
`tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs` (test
project convention: production code lives under `src/AcDream.App/...` but tests
go in `AcDream.Core.Tests` — mirrors the existing
[`EntitySpawnAdapterTests`](../../../tests/AcDream.Core.Tests/Rendering/Wb/EntitySpawnAdapterTests.cs)
location). Uses
hand-built `PhysicsScriptRunner` + capturing `ParticleHookSink` (or a thin
test double). No dats, no GL.
@ -286,9 +307,11 @@ test double). No dats, no GL.
### Integration tests — none for slice 1
The `GameWindow` wiring is two added lines (one in `OnCreateObject`, one in
`OnRemoveObject`). An integration test would require booting GL + dats +
network. Coverage is the visual verification gate instead.
The `GpuWorldState` wiring is two added lines (one in `AppendLiveEntity`, one
in `RemoveEntityByServerGuid`) plus a constructor parameter. An integration
test would require booting GL + dats + network. Coverage is the visual
verification gate instead. Existing `GpuWorldStateTests` will need a minor
update if they assert constructor arity; we extend them if so.
### Visual verification — the acceptance criterion