diff --git a/CLAUDE.md b/CLAUDE.md index 6f42fee..a7e4ec7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/docs/ISSUES.md b/docs/ISSUES.md index e4739ee..966f401 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -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. --- diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 15feb14..233e18f 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -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 30–50× 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(...)?.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: diff --git a/docs/research/2026-05-13-b4c-shipped-handoff.md b/docs/research/2026-05-13-b4c-shipped-handoff.md new file mode 100644 index 0000000..667e0ce --- /dev/null +++ b/docs/research/2026-05-13-b4c-shipped-handoff.md @@ -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.