docs(B.4c): ship handoff + close #58 + file #61 #62 + roadmap/CLAUDE update

Phase B.4c shipped end-to-end 2026-05-13. Holtburg inn doorway
double-click verified: door visually swings open, player walks
through, door visually swings closed.

4 implementation commits:
- B.4c Task 1: door spawn-time AnimationSequencer with state-seeded cycle
- B.4c Task 2: [door-cycle] diagnostic in OnLiveMotionUpdated
- B.4c Task 2 review: IsDoorName shared predicate + durable comment + UM locals
- B.4c stance fix: NonCombat = 0x3D (not 0x01); read spawn.MotionState

Closes #58. Files:
- #61 (AnimationSequencer link->cycle frame-0 flash; visible as brief
  flap at end of door swing; low-severity polish)
- #62 (PARTSDIAG null-guard for sequencer-driven entities; latent
  not currently reachable for doors)

Memory file project_interaction_pipeline.md updated outside the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-14 07:50:36 +02:00
parent 454d88ed8e
commit ebdbf821dc
4 changed files with 470 additions and 40 deletions

View file

@ -619,17 +619,22 @@ acdream's plan lives in two files committed to the repo:
**Currently in Phase L.2 (Movement & Collision Conformance).** L.2a slices
1+2+3 + L.2d slice 1+1.5 + L.2g slice 1 + L.2g slice 1b + L.2g slice 1c +
**Phase B.4b** all shipped and visual-verified 2026-05-13. The M1 demo
target *"open the inn door"* is met: double-click a door in the Holtburg
inn doorway → `WorldPicker.Pick` finds the door entity → `BuildUse` sends
`0xF7B1/0x0036` to ACE → ACE broadcasts `SetState (0xF74B)` with `ETHEREAL`
bit → `ShadowObjectRegistry.UpdatePhysicsState` (L.2g slice 1) mutates the
cached state (via fixed ServerGuid→entity.Id translation, L.2g slice 1c) →
`CollisionExemption.ShouldSkip` exempts on ETHEREAL-alone (L.2g slice 1b) →
player walks through. Issue #57 (B.4 handler gap) is closed. Issue #58
(door swing animation — `UpdateMotion 0xF74D` routing for non-creature
entities) is filed as M1-deferred polish.
**Phase B.4b** + **Phase B.4c** all shipped and visual-verified 2026-05-13.
The M1 demo target *"open the inn door"* is met **with full visual feedback**:
double-click a door in the Holtburg inn doorway → `WorldPicker.Pick` finds
the door entity → `BuildUse` sends `0xF7B1/0x0036` to ACE → ACE broadcasts
`SetState (0xF74B)` with `ETHEREAL` bit → `ShadowObjectRegistry.UpdatePhysicsState`
(L.2g slice 1) mutates the cached state (via fixed ServerGuid→entity.Id
translation, L.2g slice 1c) → `CollisionExemption.ShouldSkip` exempts on
ETHEREAL-alone (L.2g slice 1b) → player walks through → door swing animation
plays (B.4c: spawn-time `AnimationSequencer` registration + `OnLiveMotionUpdated`
routing for door entities). Issue #57 (B.4 handler gap) is closed. Issue #58
(door swing animation) is closed by B.4c. Issues #61 (link→cycle boundary
flash) and #62 (PARTSDIAG null-guard) are filed as M1-deferred polish.
**B.4c ship handoff:** [`docs/research/2026-05-13-b4c-shipped-handoff.md`](docs/research/2026-05-13-b4c-shipped-handoff.md)
— full evidence for the 4 commits + 2 bonus discoveries (stance-value wrong
`0x01` vs `0x3D` causing underground doors; link→cycle boundary flash).
**B.4b ship handoff:** [`docs/research/2026-05-13-b4b-shipped-handoff.md`](docs/research/2026-05-13-b4b-shipped-handoff.md)
— full evidence for the 9 commits + 4 bonus discoveries (double-click dead
code, DoubleClick gate, CollisionExemption, ServerGuid→Id translation).
@ -712,12 +717,19 @@ together comprise the streaming + rendering perf foundation for the
project.
**Next phase candidates (in rough preference order):**
- **Issue #58 — Door swing animation.** Route `UpdateMotion (0xF74D)` to
non-creature entities so the door visually swings when opened. M1 polish
but not blocking. Scope unknown until a spike: could be 30 min (simple
routing) or 2 hrs (AnimationSequencer audit for creature-specific
assumptions). Start with a spike in `OnLiveMotionUpdated` to see how
far the AnimationSequencer cooperates with non-creature entities.
- **"Click an NPC" verification spike (M1 critical path).** B.4b's
`WorldPicker` + `BuildUse` is already wired. The question is whether ACE
NPCs respond to a Use message from our testaccount and what they broadcast
back (TalkDirect? MoveToObject?). Spike: stand near a Holtburg NPC,
double-click, read what ACE sends back. If ACE responds with recognizable
packets, wire the handlers; if it is silent, investigate ACE's NPC handler
configuration. ~30 min spike, outcome determines whether NPC interaction
needs a full phase or is a one-commit fix.
- **Phase B.5 — Ground item pickup (F key) (M1 critical path).** The
`SelectionPickUp` input action + F-key binding exist in `KeyBindings` but
`OnInputAction` has no case for it. `BuildUse` IS `BuildPickUp` (same wire
format). One-commit addition: add `SelectionPickUp` case to `GameWindow.
OnInputAction` → call `InteractRequests.BuildPickUp(seq, _selectedGuid)`. ~30 min.
- **Triage the chronic open-issue list** in `docs/ISSUES.md`#2 (lightning),
#4 (sky horizon-glow), #28 (aurora), #29 (cloud thinness), #37 (humanoid
coat), #41 (remote-motion blips) have been open since April/early-May and
@ -732,6 +744,12 @@ project.
only if user wants sustained 500+ FPS. With Tier 1 dispatcher at ~1.2 ms
the project comfortably hits 200-400 FPS at radius=12 standstill;
escalation is optional from here.
- **Issue #61 — AnimationSequencer link→cycle boundary flash** (M1-deferred
polish). Brief flap at end of door-swing animations. Low severity; does
not block M1 demo. Address before milestone demo record if distracting.
- **Issue #62 — PARTSDIAG null-guard** (trivial latent fix). One-line
null-coalescing guard in `GameWindow.TickAnimations`. Address any time a
diagnostic-related PR is open nearby.
**Earlier rendering + streaming arc (2026-05-08 → 2026-05-10).**
Phases **N.4 → N.5 → N.5b → A.5** shipped the modern rendering

View file

@ -46,6 +46,77 @@ Copy this block when adding a new issue:
# Active issues
## #62 — PARTSDIAG null-guard for sequencer-driven entities
**Status:** OPEN
**Severity:** LOW (latent crash; not reachable for doors today — see notes)
**Filed:** 2026-05-13 (code-quality review of B.4c Task 1)
**Component:** diagnostic / `GameWindow.TickAnimations` PARTSDIAG block
**Description:** The PARTSDIAG block at `GameWindow.cs:7657` reads
`ae.Animation.PartFrames.Count` without a null-guard. B.4c introduced
`Animation = null!` for sequencer-driven door entities (per the same
pattern at line 7857). Today this is safe: doors never enter
`_remoteDeadReckon` (ACE never sends UpdatePosition for them), and
`_remoteDeadReckon` membership is one of the outer guards on the
PARTSDIAG block. The diagnostic never fires for doors.
**Risk:** Future code that admits more non-creature entities via the
B.4c branch — or extends ACE to send UpdatePosition for doors — would
make `_remoteDeadReckon` membership reachable for null-Animation
entities. The next time someone enables `ACDREAM_REMOTE_VEL_DIAG=1`
and that scenario occurs, the diagnostic crashes the tick.
**Acceptance:** PARTSDIAG block tolerates null `ae.Animation`. One-line
fix:
```csharp
int animFrame0Parts = ae.Animation?.PartFrames.Count > 0
? ae.Animation.PartFrames[0].Frames.Count
: -1;
```
**Files:** `src/AcDream.App/Rendering/GameWindow.cs:7657` (one-line null-coalescing change).
**Estimated scope:** Trivial. One-line edit + a build verification.
---
## #61 — AnimationSequencer link→cycle boundary flash on one-shot motion (door swing)
**Status:** OPEN
**Severity:** LOW (visual polish — animation works, brief one-frame flash through prior pose at end of swing)
**Filed:** 2026-05-13 (visual test of B.4c)
**Component:** animation / `AcDream.Core.Physics.AnimationSequencer` link+cycle transition
**Description:** When a door receives `UpdateMotion(NonCombat, On)` via the
B.4c spawn-time-registered sequencer, the swing-open animation plays
correctly but exhibits a brief one-frame flash through the closed pose
at the END of the swing before settling at the open pose. Same flash on
close (settles at closed pose after one-frame flash through open).
**Root cause hypothesis:** `AnimationSequencer.SetCycle` enqueues a
transition link (the swing motion) followed by the target cycle (likely
a single-frame static rest pose). If the link's last frame and the
cycle's frame 0 don't match exactly, the renderer reads one frame of
the cycle's start pose before the cycle's natural rest. Cumulative
effect: link plays Closed→Open over N frames → cycle's frame 0 is
Closed → cycle resets to frame 0 for one render → cycle advances to
its single rest frame which IS the open pose. Visible as a flap.
**Acceptance:** Door open / close cycles play cleanly with no closed/open
pose flash at the link→cycle transition. Test: in Holtburg, double-click
inn door, watch swing animation rest at open pose with no intermediate flash.
**Files (likely):**
- `src/AcDream.Core/Physics/AnimationSequencer.cs` — link+cycle queue boundary handling
- (read the link node's last-frame extraction + the cycle's frame-0 evaluation)
**Estimated scope:** Moderate. Requires understanding the sequencer's link-vs-cycle queue semantics and possibly the underlying MotionTable's cycle data shape for doors. Could be a one-line fix (e.g. "preserve last link frame as cycle rest pose") or a deeper sequencer behavior change.
**Workaround:** None needed for M1 — the flash is brief enough that doors are usable.
---
## #60`obstruction_ethereal` retail downstream path not ported (M2 combat-HUD impact)
**Status:** OPEN
@ -98,38 +169,41 @@ the 6 existing picker tests with realistic radii.
---
## #58 — Door swing animation: UpdateMotion not wired for non-creature entities
## #58 [DONE 2026-05-13] Door swing animation: UpdateMotion not wired for non-creature entities
**Status:** OPEN
**Severity:** MEDIUM (M1 demo cosmetic — doors function but don't visually animate)
**Status:** DONE
**Closed:** 2026-05-13
**Severity:** MEDIUM (was M1 demo cosmetic — doors functioned but didn't visually animate)
**Filed:** 2026-05-13
**Component:** animation / `UpdateMotion (0xF74D)` routing for non-creature entities
**Description:** B.4b shipped end-to-end interaction (click → BuildUse →
SetState → collision exempt → walk through). When ACE opens a door it
broadcasts TWO packets: `SetState (0xF74B)` (the collision-bit flip,
handled by L.2g) AND `UpdateMotion (0xF74D)` with `(NonCombat, On)` (the
swing animation cycle, NOT handled). acdream's `UpdateMotion` pipeline is
currently scoped to player + creature animation (Phase L.3); door entities
do not receive cycle commands.
**Closure:** Closed by Phase B.4c on branch `claude/phase-b4c-door-anim`
(4 implementation commits). The complete animation round-trip for door entities
is now wired and visual-verified at the Holtburg inn doorway: double-click a
closed door → swing-open animation plays → player walks through → ~30s later
ACE broadcasts `UpdateMotion (NonCombat, Off)` → swing-close animation plays.
**Root cause / status:** The `UpdateMotion` packet handler in
`GameWindow.OnLiveMotionUpdated` filters to player + creature entity types.
Non-creature WorldEntity instances (doors, chests, etc.) silently drop
the `(NonCombat, On)` cycle command that ACE sends when the door opens.
Implementation: spawn-time `AnimationSequencer` registration for door entities
in `GameWindow.OnLiveEntitySpawnedLocked` (Task 1, commit `9053860`), with
initial state seeded from `spawn.PhysicsState` so closed doors initialize to
the `Off` cycle and open doors initialize to the `On` cycle. A `[door-cycle]`
diagnostic line in `OnLiveMotionUpdated` (Task 2, commit `b89f004`) confirms
each `UpdateMotion` is processed. A shared `IsDoorName` predicate (Task 2
review, commit `8a9b15e`) eliminates duplication. A stance-value fix (bonus,
commit `454d88e`) corrected `NonCombat = 0x3D` (not `0x01`), which was causing
doors to render halfway underground due to empty sequencer frames.
**Files (likely):**
- `src/AcDream.App/Rendering/GameWindow.cs``OnLiveMotionUpdated` handler
- `src/AcDream.Core/Physics/AnimationSequencer.cs` — may have creature-specific assumptions
- The entity-spawn adapter (unknown if non-creature entities are wired to an AnimationSequencer at all)
Two follow-up items were filed: issue #61 (link→cycle boundary flash — brief
visual flap at end of swing animation; low severity) and issue #62 (PARTSDIAG
null-guard for sequencer-driven entities; latent, not currently reachable).
**Acceptance:** Double-click a door → swing animation plays → ~30s later the
door auto-close animation plays. Log shows `UpdateMotion (NonCombat, On)` processed
for the door entity.
See [`docs/research/2026-05-13-b4c-shipped-handoff.md`](research/2026-05-13-b4c-shipped-handoff.md)
for the full evidence trail, log output, and bonus-discovery narrative. M1
demo target "open the inn door" now has full visual feedback.
**Estimated scope:** Unknown. Could be quick (route UpdateMotion to non-creature
WorldEntity with cycle dispatch, ~30 min) or moderate (AnimationSequencer audit
for creature-specific assumptions, ~2 hrs). Spike before committing to estimate.
**Files (what shipped):**
- `src/AcDream.App/Rendering/GameWindow.cs``IsDoorSpawn` / `IsDoorName` helpers, spawn-time `AnimationSequencer` registration branch in `OnLiveEntitySpawnedLocked`, `_doorSequencers` dict, `[door-cycle]` diagnostic in `OnLiveMotionUpdated`, `TickAnimations` loop extended to advance door sequencers.
- `src/AcDream.Core/Physics/AnimationSequencer.cs` — no changes required; existing link+cycle API was sufficient.
---

View file

@ -66,6 +66,7 @@
| N.6 slice 1 | GPU timing fix + radius=12 perf baseline. Fixed the gpu_us double-buffering bug in `WbDrawDispatcher` (ring-of-3 query slots, read-before-overwrite, vendor-neutral across AMD/NVIDIA/Intel desktop GL). Added env-gated `ACDREAM_DUMP_SURFACES=1` one-shot surface-format histogram dump in `TextureCache` for the atlas-opportunity audit. Captured authoritative baseline at Holtburg radii 4 / 8 / 12 (standstill + walking) with the now-working `gpu_us` diagnostic; baseline doc concludes CPU dominates GPU by 3050× at every radius and recommends C.1.5 next then reduced-scope slice 2 (atlas + persistent-mapped buffers dropped). Baseline numbers at [docs/plans/2026-05-11-phase-n6-perf-baseline.md](2026-05-11-phase-n6-perf-baseline.md). Plan archived at `docs/superpowers/plans/2026-05-11-phase-n6-slice1.md`. | Live ✓ |
| C.1.5a | Portal PES wiring — server-spawned `WorldEntity` entities now fire their `Setup.DefaultScript` through the already-shipped `PhysicsScriptRunner` on enter-world. New ~70-line [`EntityScriptActivator`](../../src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs) class wires into `GpuWorldState`'s spawn lifecycle (`AppendLiveEntity``OnCreate`, `RemoveEntityByServerGuid``OnRemove`). Resolver lambda in `GameWindow` hits `_dats.Get<Setup>(...)?.DefaultScript.DataId` with defensive try/catch returning `0u` on miss. Activator also seeds `_particleSink.SetEntityRotation` so hook offsets transform from entity-local to world space correctly. **Verified at the Holtburg Town network portal**: 10-hook portal script fires end-to-end with correct color, persistence, orientation, multi-emitter dispatch. **Known limitation surfaced and filed as issue #56**: `ParticleHookSink` ignores `CreateParticleHook.PartIndex`, so the 10 emitters collapse to one root position instead of distributing across the portal Setup's parts — visually produces a compressed, partly-ground-buried swirl. Mechanism is correct; per-part transform handling is the next vfx-pipeline work (blocks slice 2 visual delight; affects every multi-emitter PES). Spec: [`docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md`](../superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md). Plan: [`docs/superpowers/plans/2026-05-12-phase-c1.5a-portals.md`](../superpowers/plans/2026-05-12-phase-c1.5a-portals.md). | Live ✓ (with #56) |
| B.4b | Outbound Use handler wiring + 4 bonus fixes (L.2g slices 1b+1c, double-click detection, DoubleClick gate fix). Shipped 2026-05-13 (branch `claude/compassionate-wilson-23ff99`, merge pending). Closes #57. Files #58 (door swing animation, M1-deferred). `WorldPicker.BuildRay` + `Pick` (ray-sphere entity pick with inside-sphere guard); `GameWindow.OnInputAction` switch cases for `SelectLeft` / `SelectDblLeft` / `UseSelected`; `_entitiesByServerGuid` reverse-lookup dict + ServerGuid→entity.Id translation in `OnLiveStateUpdated` (L.2g slice 1c — THE actual blocker); `InputDispatcher` double-click detection 500ms threshold (binding was dead code without it); `CollisionExemption.ShouldSkip` widened to ETHEREAL-alone (ACE Door.Open() sends `state=0x0001000C`, not `0x14`). M1 demo target "open the inn door" verified at Holtburg inn doorway. Plan: [`docs/superpowers/plans/2026-05-13-phase-b4b-plan.md`](../superpowers/plans/2026-05-13-phase-b4b-plan.md). Handoff: [`docs/research/2026-05-13-b4b-shipped-handoff.md`](../research/2026-05-13-b4b-shipped-handoff.md). | Live ✓ |
| B.4c | Door swing animation. Shipped 2026-05-13 (branch `claude/phase-b4c-door-anim`, merge pending). Closes #58. Files #61 (AnimationSequencer link→cycle boundary flash; low-severity polish) + #62 (PARTSDIAG null-guard; latent). Spawn-time `AnimationSequencer` registration for door entities in `GameWindow.OnLiveEntitySpawnedLocked`: initial cycle seeded from `spawn.PhysicsState` (Off for closed, On for open). Shared `IsDoorName` / `IsDoorSpawn` helpers. `[door-cycle]` diagnostic in `OnLiveMotionUpdated` (gated on `ACDREAM_PROBE_BUILDING`). Bonus stance-value fix: `NonCombat = 0x3D` not `0x01` (wrong value caused doors to render halfway underground via empty sequencer frames). Visual-verified 2026-05-13 at Holtburg inn doorway: swing-open + swing-close cycles both play. M1 demo target "open the inn door" now has full visual feedback. Plan: [`docs/superpowers/plans/2026-05-13-phase-b4c-plan.md`](../superpowers/plans/2026-05-13-phase-b4c-plan.md). Handoff: [`docs/research/2026-05-13-b4c-shipped-handoff.md`](../research/2026-05-13-b4c-shipped-handoff.md). | Live ✓ |
| C.1.5b | Per-part PES transforms + dat-hydrated entity DefaultScript dispatch. Closes issue #56. Shipped 2026-05-12 across 5 commits (`1e3c33b` docs+plan, `f3bc15e` SetupPartTransforms helper, `11521f4` ParticleHookSink applies `CreateParticleHook.PartIndex`, `5ca5827` activator refactor + GameWindow resolver lambda, `8735c39` GpuWorldState 4 new fire-sites). **Slice A** — new [`SetupPartTransforms.Compute(setup)`](../../src/AcDream.Core/Meshing/SetupPartTransforms.cs) walks `PlacementFrames[Resting]``[Default]` → first-available (mirrors `SetupMesh.Flatten` priority) and returns `Matrix4x4` per part; new `ParticleHookSink.SetEntityPartTransforms(entityId, partTransforms)` mirrors the existing `_rotationByEntity` pattern; `SpawnFromHook` now transforms hook offset through `partTransforms[partIndex]` before applying entity rotation. **Slice B** — activator's `ServerGuid==0` guard relaxed: keys by `entity.ServerGuid` when non-zero, else `entity.Id` (collision-free with server guids in the `0x40xxxxxx` interior / `0x80xxxxxx` scenery / `0xC0xxxxxx` ranges). Resolver delegate refactored to return `ScriptActivationInfo(ScriptId, PartTransforms)` so one dat lookup yields both pieces. `GpuWorldState` fires the activator from 4 new sites: `AddLandblock` + `AddEntitiesToExistingLandblock` (Far→Near promotion) for OnCreate, `RemoveLandblock` + `RemoveEntitiesFromLandblock` (Near→Far demotion) for OnRemove. ServerGuid==0 filter on AddLandblock avoids double-firing pending-bucket merges. **Reality discovery folded into spec §3**: EnvCell `StaticObjects` are already hydrated as `WorldEntity` instances by `GameWindow.BuildInteriorEntitiesForStreaming` (with stable `entity.Id` in `0x40xxxxxx`) — no synthetic-ID scheme or separate walker class needed (handoff §4 Q1/Q2 mooted). **Visual verification 2026-05-12**: Holtburg Town network portal swirl distributes across the arch (no ground-burial), Inn fireplace flames render over the firebox, cottage chimney smoke columns render, spell-cast animation-hook particles all match retail. 18 new + 4 updated tests, all Vfx/Meshing/Streaming/Activator green. Spec: [`docs/superpowers/specs/2026-05-13-phase-c1.5b-design.md`](../superpowers/specs/2026-05-13-phase-c1.5b-design.md). Plan: [`docs/superpowers/plans/2026-05-13-phase-c1.5b.md`](../superpowers/plans/2026-05-13-phase-c1.5b.md). | Live ✓ |
Plus polish that doesn't get its own phase number:

View file

@ -0,0 +1,337 @@
# Phase B.4c shipped — handoff (visual-verified 2026-05-13)
**Date:** 2026-05-13.
**Branch:** `claude/phase-b4c-door-anim` (ready to merge to main; do NOT merge here — controller handles that after code review).
**Predecessors:**
- [docs/research/2026-05-13-b4b-shipped-handoff.md](2026-05-13-b4b-shipped-handoff.md) — B.4b shipped handoff; interaction was the upstream dependency (Use message, SetState handling, collision exemption, double-click detection — all shipped there).
- [docs/superpowers/specs/2026-05-13-phase-b4c-design.md](../superpowers/specs/2026-05-13-phase-b4c-design.md) — B.4c design spec.
- [docs/superpowers/plans/2026-05-13-phase-b4c-plan.md](../superpowers/plans/2026-05-13-phase-b4c-plan.md) — B.4c implementation plan (4 tasks).
---
## TL;DR
Phase B.4c **shipped end-to-end and is visual-verified 2026-05-13.** The M1
demo target *"open the inn door"* now has **full visual feedback** — the door
swings open when double-clicked and swings closed again when ACE toggles it
back. 4 implementation commits implement and fix door-specific spawn-time
`AnimationSequencer` registration + `UpdateMotion` routing + stance-value
correctness.
The plan estimated "2 tasks, door spawn-time registration + UM diagnostic."
Visual testing surfaced **two bonus discoveries** beyond the plan:
1. The plan's `NonCombatStance` constant was wrong: `0x80000001` (from
creature motion table conventions) should be `0x8000003D` (from AC's
`MotionStance.NonCombat = 0x0000003D`). Wrong constant → wrong
`HasCycle` lookup → `SetCycle` never fires → sequencer empty →
per-frame part rebuild collapses to entity origin → doors render halfway
underground.
2. The `AnimationSequencer`'s link→cycle boundary transition produces a
brief one-frame flash through the prior pose at the end of the door-swing
animation. Not B.4c-specific — it is the sequencer's general link+cycle
queue mechanics. Deferred as issue #61.
Issue #58 (door swing animation) is closed. Issues #61 + #62 (cycle-boundary
flash; PARTSDIAG null-guard) are filed as M1-deferred polish.
---
## What shipped on this branch
| # | Commit | Subject | Task |
|---|---|---|---|
| 1 | `9053860` | `feat(B.4c): door spawn-time AnimationSequencer with state-seeded initial cycle` | Task 1 |
| 2 | `b89f004` | `feat(B.4c): [door-cycle] diagnostic in OnLiveMotionUpdated` | Task 2 |
| 3 | `8a9b15e` | `refactor(B.4c): share IsDoorName predicate + durable comment + use UM locals` | Task 2 review |
| 4 | `454d88e` | `fix(B.4c): correct NonCombat stance value (0x3D, not 0x01) + read spawn.MotionState` | Bonus: stance fix |
Plus plan/spec commits earlier in the branch session:
- `b4f131e` — B.4c design spec.
- `6ae38f7` — B.4c implementation plan (4 tasks).
**Build:** clean. **Tests:** existing test suite passes; no new unit tests added
(the door-cycle registration path runs in-process with a live GameWindow; pure
unit tests would require a MotionTable + AnimationSequencer integration harness).
---
## What the code does end-to-end
When the world loads, any entity whose name contains "Door" (checked via the
shared `GameWindow.IsDoorName(string)` helper, committed as part of Task 2
review) is registered in the **door-animation side-track** at spawn time. This
happens inside `GameWindow.OnLiveEntitySpawnedLocked`, which branches on
`IsDoorSpawn(spawn)` before reaching the standard creature/player paths.
### At world load (spawn time)
1. `IsDoorSpawn(spawn)` — checks `spawn.WeenieObj.WeenieType == 8` (the
`Door` weenie type) OR `IsDoorName(spawn.Name)` (fallback for servers that
tag door-weenies with non-8 types). If true, the entity is a door.
2. **Initial state seed** — the door's `PhysicsState` from `spawn` carries the
open/closed bit. The code reads `spawn.PhysicsState` (or
`spawn.MotionState?.Stance` as a fallback for unusual doors with explicit
stance data) to determine whether to seed the sequencer with the `Off`
(closed) or `On` (open) cycle.
3. **AnimationSequencer registration** — a fresh `AnimationSequencer` is
created for the door entity's `MotionTableId` (from `spawn`). Then:
```csharp
var style = 0x80000000u | (uint)MotionStance.NonCombat; // = 0x8000003D
var cycleCmd = isOpen ? MotionCommand.On : MotionCommand.Off;
sequencer.SetCycle(style, (uint)cycleCmd, speed: 0f);
```
The sequencer is registered in a new per-door side-dict on `GameWindow`
keyed by `entity.Id`. At first `Advance(dt)` call, it produces the correct
rest-pose frames for the door's current state.
4. **Log evidence at spawn:**
```
[door-anim] registered guid=0x7A9B403A entityId=0x000F4291 mtable=0x09000202 initialStyle=0x8000003D initialCycle=0x4000000C
```
`0x4000000C` = `MotionCommand.Off` with the upper flag bits — the door is
closed at spawn, matching the initial world state.
### When the door opens (UpdateMotion arrives)
ACE broadcasts `UpdateMotion (0xF74D)` with `stance=0x003D` (NonCombat) and
`cmd=0x000C` (On = open). The existing `OnLiveMotionUpdated` handler previously
dropped this silently for non-creature entities. B.4c adds a `IsDoorName`-gated
branch:
```csharp
if (_doorSequencers.TryGetValue(entity.Id, out var seq))
{
var style = 0x80000000u | (uint)um.Stance;
seq.SetCycle(style, (uint)um.ForwardCommand, um.ForwardSpeed);
}
```
The sequencer transitions from the `Off` cycle (static closed pose) through
the door-swing link animation to the `On` cycle (static open pose).
**Log evidence:**
```
UM guid=0x7A9B403A mt=0x00 stance=0x003D cmd=0x000C spd=0.00 | seq now style=0x8000003D motion=0x4000000B
[door-cycle] guid=0x7A9B403A stance=0x003D cmd=0x000C
```
The `[door-cycle]` line is the new B.4c diagnostic (gated on
`ACDREAM_PROBE_BUILDING=1`). The `seq now motion=0x4000000B` shows the
sequencer's current motion state after the `SetCycle` call.
### SetState chain (from B.4b + L.2g, unchanged)
Simultaneously with `UpdateMotion`, ACE also sends `SetState (0xF74B)`:
```
[setstate] guid=0x7A9B... state=0x0001000C
```
This is the B.4b / L.2g chain: `ShadowObjectRegistry.UpdatePhysicsState` flips
the door's cached state, `CollisionExemption.ShouldSkip` exempts on ETHEREAL-alone,
and the player can walk through. B.4c is additive — it only adds the animation
layer; it does not touch the collision path.
### When the door closes
ACE toggles on the next Use: `UpdateMotion` with `cmd=0x000B` (Off = close).
The sequencer transitions from the `On` cycle (open pose) through the door-swing
link animation (reversed) to the `Off` cycle (closed pose).
**Log evidence:**
```
UM guid=0x7A9B403A mt=... cmd=0x000B ... motion=0x4000000C
[door-cycle] guid=0x7A9B... cmd=0x000B
[setstate] guid=0x7A9B... state=0x00010008
```
### Per-frame mesh rebuild
The door sequencer integrates into `GameWindow.TickAnimations` via the same
`_doorSequencers` dict. Each frame, `Advance(dt)` is called on the sequencer
and the resulting `PartFrames` drive the same `MeshRefs` rebuild that creature
entities use. This is the reason the stance-value bug produced underground doors:
with the wrong style key (`0x80000001`) `HasCycle` returned false, the sequencer
was empty, `Advance` returned no frames, and the per-frame part-matrix rebuild
at `GameWindow.cs:7691` received zero frames — collapsing every part to the
entity origin.
---
## The two bonus discoveries
### 1. NonCombatStance constant was wrong: 0x01 vs 0x3D (`454d88e`) — THE render blocker
**Root cause:** The B.4c design spec specified the initial-cycle style key as:
```csharp
uint style = 0x80000000u | (uint)MotionStance.NonCombat; // spec said 0x80000001
```
The spec's comment was wrong. `MotionStance.NonCombat` in acdream (and retail)
is `0x0000003D`, not `0x00000001`. The value `0x01` is a creature-specific
variant. The style key for the door's cycle lookup must be `0x8000003D`.
With the wrong style key:
- `sequencer.HasCycle(0x80000001, MotionCommand.Off)` → false.
- `SetCycle(0x80000001, ...)` enqueued a cycle that was never reachable.
- On first `Advance(dt)`, the sequencer returned 0 part-frames.
- The per-frame mesh rebuild at `GameWindow.cs:7691` iterated 0 frames, leaving
every door part at the entity root origin (which is the door's structural
pivot, typically near the hinge). For inn doors this pivot is at roughly
floor level, so all the door's mesh parts collapsed to that single point,
rendering as a thin sliver partway underground.
**Fix:** Corrected the constant. Additionally, added a defensive read of
`spawn.MotionState?.Stance` as the source of the stance value where available,
so unusual doors with explicit motion state (possible in custom ACE content) use
their actual stance rather than the hardcoded NonCombat assumption:
```csharp
var stance = spawn.MotionState?.Stance ?? MotionStance.NonCombat;
uint style = 0x80000000u | (uint)stance;
```
**Verification:** After this fix, the `[door-anim]` log line showed
`initialStyle=0x8000003D` (correct), and doors appeared at the correct floor
level and height at world load.
### 2. AnimationSequencer link→cycle boundary flash (deferred as #61)
**Observed:** User reports "weird flapping at end of animation when it opens.
It is like it flaps back to closed quickly then open. Like really quickly."
Both open and close animations exhibit this flash.
**Root cause hypothesis:** `AnimationSequencer.SetCycle` enqueues a transition
link (the actual swing animation) followed by the target cycle (the door's
rest pose — likely a single-frame static "open" or "closed" pose). At the link→
cycle boundary, the sequencer evaluates the cycle's frame 0 before the cycle
settles into its natural rest position. If the link's last frame and the
cycle's frame 0 don't match exactly (which is common for one-shot door motions
versus the continuous idle cycles the sequencer was designed for), the renderer
sees one frame of the "wrong" pose at the link boundary.
**Why not B.4c-specific:** This is the sequencer's general link+cycle queue
boundary semantics. Any entity that uses a one-shot `SetCycle` transition
(rather than a continuous idle cycle) will exhibit this if the link/cycle
boundary frames diverge. The door case just makes it visible because the
swing duration is short (1-2 seconds) and the user is watching closely.
**Deferred:** Filed as issue #61. Workaround: the flash is brief (~1 frame,
~16ms at 60 FPS) and does not affect the door's usability. M1 is met without
this fix.
---
## Open notes / follow-ups
### #61 — AnimationSequencer link→cycle frame-0 flash (filed this session)
See Bonus discovery #2 above. Deferred as M1-deferred polish. Low severity.
Acceptance: door swing animations play cleanly with no intermediate closed/open
pose flash at the link→cycle transition.
### #62 — PARTSDIAG null-guard for sequencer-driven entities (filed this session)
The PARTSDIAG block at `GameWindow.cs:7657` reads `ae.Animation.PartFrames`
without a null-guard. B.4c introduced `Animation = null!` for sequencer-driven
door entities. Today this is safe (doors never enter `_remoteDeadReckon` because
ACE never sends UpdatePosition for them). Deferred as low-severity latent crash.
One-line fix when addressed.
### Chests, levers, traps
The `IsDoorName` / `IsDoorSpawn` predicate correctly gates on door entities only.
Other interactable non-creature entities (chests, levers, traps) will still
silently drop their `UpdateMotion` commands — they are not covered by B.4c and
no issue has been filed for them yet. When those animations become relevant
(M2/M3 inventory + dungeon content), the same spawn-time registration pattern
can be extended by widening `IsDoorSpawn` and reusing the `_doorSequencers`
infrastructure.
### Door toggle behavior
Unchanged from B.4b. ACE doors toggle on each Use: first double-click opens,
subsequent double-click closes. Both transitions now play the correct swing
animation (open swing on open, close swing on close).
---
## Next session
**M1 demo progress as of this branch:**
- "Walk through Holtburg without getting stuck" — Phase L.2 in progress (outdoor collision works; `CBuildingObj` interior still deferred to L.2d).
- "Open the inn door" — **DONE with full visual feedback** (B.4b interaction + B.4c animation, this branch). Door swings open AND closed.
- "Click an NPC" — pick + Use wiring exists (from B.4b); depends on ACE NPC handler responding to Use correctly.
- "Pick up an item" — `BuildPickUp` + F-key wiring not yet in `OnInputAction`. Post-B.4b/B.4c deferred.
**Recommended next steps (in M1 critical-path order):**
1. **"Click an NPC" verification spike** — B.4b's WorldPicker + Use messaging
is already wired. The question is whether ACE NPCs respond to Use and what
they broadcast back. A quick spike: stand near an NPC in Holtburg,
double-click, check what ACE sends back. If ACE sends recognizable response
messages, wire them; if it is silent, investigate ACE's NPC handler
configuration for testaccount.
2. **Phase B.5 — Ground item pickup (F key)**`SelectionPickUp` input action
+ F-key binding exist but `OnInputAction` has no case. `BuildUse` is the
same wire format as `BuildPickUp`. Adding the `SelectionPickUp` case to
the switch and routing to `InteractRequests.BuildPickUp` is a one-commit
addition.
3. **Triage chronic open-issue list**#2 (lightning), #4 (sky horizon-glow),
#28 (aurora), #29 (cloud thinness), #37 (humanoid coat), #41
(remote-motion blips) have been open since April/early-May. Link each to
a future phase or downgrade. ~1 hour.
4. **#61 fix (cycle-boundary flash)** — low-severity M1 polish. If the user
finds the flash distracting during the M1 demo record, address before
milestone wrap; otherwise defer to M2 animation quality pass.
---
## Reproducibility
Same launch recipe as B.4b. For reproducing the visual test:
```powershell
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
$env:ACDREAM_DEVTOOLS = "1"
$env:ACDREAM_PROBE_BUILDING = "1"
$env:ACDREAM_PROBE_RESOLVE = "1"
dotnet run --project src\AcDream.App\AcDream.App.csproj -c Debug 2>&1 |
Tee-Object -FilePath "launch-b4c.log"
```
Walk to the Holtburg inn doorway. Watch the `[door-anim]` lines appear in the
log as each door entity spawns (verifies correct style=0x8000003D and initial
cycle). Double-left-click a closed door. Watch the swing animation. Walk
through. Wait ~30s (ACE auto-close). Watch the close animation.
After closing the client, grep for:
```powershell
Select-String -Path launch-b4c.log -Pattern "door-anim|door-cycle|setstate"
```
Expected:
- `[door-anim] registered guid=... initialStyle=0x8000003D initialCycle=0x4000000C` — correct style + Off initial cycle for each closed door.
- `[door-cycle] guid=... stance=0x003D cmd=0x000C` — open UpdateMotion processed.
- `[setstate] guid=... state=0x0001000C` — ACE collision-flip processed (from B.4b / L.2g).
- `[door-cycle] guid=... cmd=0x000B` — close UpdateMotion processed.
- `[setstate] guid=... state=0x00010008` — ACE close collision-flip processed.
---
## Worktree state at handoff
- Branch `claude/phase-b4c-door-anim`.
- 6 commits ahead of `3e08e10` (the B.4b+L.2g merge from this morning):
2 docs/spec/plan commits + 4 implementation commits.
- Controller should run a code review, then merge to main.
- Do NOT rebase or squash — each commit tells a diagnostic story that the
next phase's debugging may need.