docs(vfx #C.1.5b): ship Phase C.1.5b — closes #56 + EnvCell DefaultScript dispatch

Roadmap:
- Status header now reads "Between phases" with C.1.5b in the recent-wins
  bullet list.
- New SHIPPED row in the table (C.1.5b) summarising both slices, the
  reality discovery about EnvCell statics, and the 4 visual-verified sites.
- The "IN FLIGHT — C.1.5b" sub-bullet under Phase C flipped to SHIPPED.

ISSUES.md:
- #56 removed from Active.
- #56 added to Recently closed with full commit chain (1e3c33b spec+plan,
  f3bc15e SetupPartTransforms helper, 11521f4 ParticleHookSink part-transform,
  5ca5827 activator refactor, 8735c39 GpuWorldState fire-sites), resolution
  notes, and the visual-verification record.

CLAUDE.md:
- "Currently in flight" pointer replaced by a "Currently between phases"
  marker + a C.1.5b shipped paragraph that's parallel to the existing
  C.1.5a entry.
- "After C.1.5b" → "Next phase candidates".

Visual verification 2026-05-12: portal swirl matches retail extent + lateral
spread (no ground-burial); Holtburg Inn fireplace flames; cottage chimney
smoke; spell-cast particles on +Acdream — all match retail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-12 08:51:26 +02:00
parent 8735c39a40
commit c988e98b5a
3 changed files with 55 additions and 45 deletions

View file

@ -46,37 +46,6 @@ Copy this block when adding a new issue:
# Active issues
## #56`ParticleHookSink` ignores `CreateParticleHook.PartIndex`; multi-emitter scripts collapse to entity root
**Status:** OPEN
**Severity:** MEDIUM (every multi-emitter PES on a multi-part entity is visually wrong — portals, chimneys, fireplaces, animation-hook spell effects, anything where the dat author distributed emitters across mesh parts)
**Filed:** 2026-05-12
**Component:** vfx / `ParticleHookSink` (call-site contract gap with the renderer)
**Description:** When `EntityScriptActivator` (Phase C.1.5a) fires a multi-emitter `PhysicsScript` such as a portal's `DefaultScript`, every `CreateParticleHook` in the script spawns at `entity.Position + rotated(hook.Offset.Origin)` — ignoring the hook's `PartIndex`. The Holtburg Town network portal's script `0x3300126D` has 10 hooks (8 `CreateParticle` + sounds + sub-script calls) intended to attach to different mesh parts of the Setup (arch base, columns, apex). All 10 collapse to one point, producing a compressed, ground-buried swirl instead of the multi-tier shape retail renders.
Captured during C.1.5a visual verification 2026-05-12:
- Portal A: entity `0x7A9B405B`, script `0x3300126D`, anchor `(27.33, 137.49, 66.30)`, 10 hooks
- Portal B: entity `0x7A9B4080`, script `0x3300067A`, anchor `(14.39, 55.61, 78.20)`, 4 hooks
User report: "It's less flat [than pre-rotation-fix] but in retail it seems to expand more in all directions. … still buried in the ground."
**Root cause / status:** Documented in [ParticleHookSink.cs:18-24](../src/AcDream.Core/Vfx/ParticleHookSink.cs#L18) as a known C.1 limitation: "Retail attaches to a specific mesh part; we attach to the entity's root and will refine per-part when the renderer exposes per-part world transforms." The renderer (`WbDrawDispatcher`) does compute per-part transforms each frame for the modern bindless path, but they're not surfaced to the sink. The activator passes only `entity.Position` + `entity.Rotation`; the part-relative offsets the dat author chose are lost.
**Files:**
- [src/AcDream.Core/Vfx/ParticleHookSink.cs:176-217](../src/AcDream.Core/Vfx/ParticleHookSink.cs) (`SpawnFromHook` — currently `anchor = worldPos + Vector3.Transform(offset, rotation)`; missing the `part[PartIndex].Transform` multiplication).
- [src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs](../src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs) (would need to pass per-part transforms, or arrange for the sink to query them).
- Renderer-side: per-part transforms live in `WbDrawDispatcher` / `AnimatedEntityState`.
**Research:** For static entities (portals, chimneys, fireplaces), per-part offsets can be precomputed from `Setup.PlacementFrames[Resting]` at spawn time — no animation tick needed. For animated entities, the per-part transform varies per frame and the sink would need a per-tick refresh similar to how `UpdateEntityAnchor` works for AttachLocal emitters today.
**Acceptance:** The Holtburg Town network portal's swirl matches retail in vertical extent (no ground-burial) and lateral spread (multiple emitters at distinct part positions, not collapsed to one point). Side-by-side dual-client visual check, same procedure as the C.1.5a acceptance gate.
**Blocks / unblocks:**
- Phase C.1.5b (EnvCell static chimneys + fireplaces) will visually disappoint without this fix — chimneys are multi-part dat objects with smoke emitters attached to the chimney-top part.
- Phase C.1.5a (portal wiring) shipped without it because the mechanism is correct end-to-end and the gap is a separate concern that benefits every multi-part PES path.
---
## #55 — Static-entity slow path reports ~1.45M `meshMissing` per 5s at r4 standstill
**Status:** OPEN
@ -1817,6 +1786,26 @@ Unverified. The likely culprits, ranked by suspected probability:
# Recently closed
## #56 — [DONE 2026-05-12 · 8735c39] `ParticleHookSink` ignores `CreateParticleHook.PartIndex`; multi-emitter scripts collapse to entity root
**Closed:** 2026-05-12
**Commit chain (newest first):**
- `8735c39` — feat(vfx #C.1.5b): GpuWorldState fires activator for dat-hydrated entities (4 new fire-sites + 5 integration tests; also picks up EnvCell statics & exterior stabs as a side-effect of the activator-guard relaxation)
- `5ca5827` — feat(vfx #C.1.5b): activator handles dat-hydrated entities + per-part transforms (resolver returns `ScriptActivationInfo(ScriptId, PartTransforms)`; keys by ServerGuid OR entity.Id; GameWindow resolver lambda upgraded; 4 existing + 3 new tests)
- `11521f4` — fix(vfx #56): `ParticleHookSink` applies `CreateParticleHook.PartIndex` transform (new `_partTransformsByEntity` side-table; `SpawnFromHook` transforms offset through `partTransforms[PartIndex]` before applying entity rotation; 2 new tests + 2 existing pass)
- `f3bc15e` — feat(vfx #C.1.5b): `SetupPartTransforms` helper for per-part anchor transforms (walks `PlacementFrames[Resting]``[Default]` → first-available; 4 tests)
- `1e3c33b` — docs(vfx #C.1.5b): design + plan for issue #56 + EnvCell DefaultScript
**Component:** vfx / `ParticleHookSink` + `EntityScriptActivator` + `GpuWorldState` + `SetupPartTransforms`
**Resolution.** Two-slice fix that also folded in slice 2 of the C.1.5 phase work. **Slice A (the #56 fix proper)**: precomputed per-part `Matrix4x4` array at activator-spawn time via the new `SetupPartTransforms.Compute(setup)` helper, threaded through `EntityScriptActivator``ParticleHookSink.SetEntityPartTransforms(entityId, partTransforms)` (mirrors the existing `_rotationByEntity` side-table pattern), applied inside `SpawnFromHook` as `partLocal = Transform(offset, partTransforms[PartIndex])` before the existing world-rotation step. Backwards-compatible: entities without registered part transforms fall through to identity (pre-fix behavior). **Slice B (folded in same phase, makes the fix matter for slice 2 visual gates)**: dropped the activator's `ServerGuid==0` early-return guard. Activator now keys by `entity.ServerGuid` when non-zero, else `entity.Id` — collision-free because dat-hydrated entity IDs live in the `0x40xxxxxx` (interior) / `0x80xxxxxx` (scenery) / `0xC0xxxxxx` ranges, all disjoint from server guids. `GpuWorldState` fires the activator from 4 new sites: `AddLandblock` + `AddEntitiesToExistingLandblock` (Far→Near promotion) for OnCreate, `RemoveLandblock` + `RemoveEntitiesFromLandblock` (Near→Far demotion) for OnRemove. Live entities are filtered out by `ServerGuid != 0` on the `AddLandblock` path so pending-bucket merges don't double-fire OnCreate.
**Reality discovery folded into spec §3:** the handoff doc's §4 Q1/Q2 (synthetic-ID scheme + new walker class) were mooted by finding that `GameWindow.BuildInteriorEntitiesForStreaming` already hydrates EnvCell `StaticObjects` as `WorldEntity` instances with stable `entity.Id`. No new walker, no synthetic IDs.
**Verification.** Build green. 77 Vfx+Meshing+Activator+Streaming tests pass (4 new for SetupPartTransforms + 2 new for ParticleHookSink + 4 updated + 3 new for activator + 5 new for GpuWorldState integration). 8 pre-existing Physics/Input failures unchanged (verified by stash-and-rerun on Task 4). **Visual verification 2026-05-12**: Holtburg Town network portal (entity `0x7A9B405B`, script `0x3300126D`) — swirl no longer ground-buried, emitters distributed across the arch; Holtburg Inn fireplace flames over the firebox; cottage chimney smoke; spell cast on `+Acdream` cast-anim particles — all match retail.
**Acceptance reproducer:** the C.1.5a verification log captured portal A entity `0x7A9B405B` swirl compressed to a partly-ground-buried point. Post-fix at the same portal, the swirl extends through the arch in retail-matching shape.
## #53 — [DONE 2026-05-11 · f928e66] A.5/tier1-redo: entity-classification cache retry
**Closed:** 2026-05-11