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:
parent
06d7fbd5ef
commit
ed5335b81e
2 changed files with 694 additions and 20 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue