diff --git a/docs/research/2026-05-13-b4c-shipped-handoff.md b/docs/research/2026-05-13-b4c-shipped-handoff.md index 667e0ce..862e4be 100644 --- a/docs/research/2026-05-13-b4c-shipped-handoff.md +++ b/docs/research/2026-05-13-b4c-shipped-handoff.md @@ -66,9 +66,11 @@ happens inside `GameWindow.OnLiveEntitySpawnedLocked`, which branches on ### 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. +1. `IsDoorSpawn(spawn)` — delegates to `IsDoorName(spawn.Name)`, which + returns `name == "Door"`. Detection by server-sent name string only. + Cheap, exact, no WeenieType lookup. If a future ACE localizes "Door" + or sends a different name, those entities silently won't animate — + acceptable per B.4c's "doors only at English Holtburg" scope. 2. **Initial state seed** — the door's `PhysicsState` from `spawn` carries the open/closed bit. The code reads `spawn.PhysicsState` (or @@ -83,8 +85,13 @@ happens inside `GameWindow.OnLiveEntitySpawnedLocked`, which branches on 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 + The fully-initialized `AnimatedEntity` (with the seeded `Sequencer`) is + registered into the existing `_animatedEntities` dict keyed by `entity.Id` + — same dict that holds creatures and the player. `Animation = null!` + (the null-forgiving suppression matches an existing pattern at + `GameWindow.cs:7885` for sequencer-driven entities where the legacy + `Animation` field is unused). At the first per-frame `Advance(dt)` + call from `TickAnimations`, the sequencer produces the correct rest-pose frames for the door's current state. 4. **Log evidence at spawn:** @@ -97,17 +104,17 @@ happens inside `GameWindow.OnLiveEntitySpawnedLocked`, which branches on ### 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: +wire `cmd=0x000C` (which `MotionCommandResolver.ReconstructFullCommand` +maps to full motion `0x4000000B` = `MotionCommand.On` = door open). -```csharp -if (_doorSequencers.TryGetValue(entity.Id, out var seq)) -{ - var style = 0x80000000u | (uint)um.Stance; - seq.SetCycle(style, (uint)um.ForwardCommand, um.ForwardSpeed); -} -``` +B.4c does NOT add a new dispatch path here — the existing +`OnLiveMotionUpdated` handler already routes via the `_animatedEntities` +dict + per-entity `Sequencer`, the same code path creatures use. The +only B.4c contribution at UM dispatch is the new `[door-cycle]` +diagnostic gated on `IsDoorName(doorInfo.Name)`. Before B.4c, doors +silently dropped at the `_animatedEntities.TryGetValue` check at +`GameWindow.cs:3036` because doors weren't registered; B.4c's Task 1 +spawn-time branch fixed that. The sequencer transitions from the `Off` cycle (static closed pose) through the door-swing link animation to the `On` cycle (static open pose). @@ -148,13 +155,14 @@ UM guid=0x7A9B403A mt=... cmd=0x000B ... motion=0x4000000C ### 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. +`_animatedEntities` dict that holds creatures. Each frame, `ae.Sequencer.Advance(dt)` +is called and the resulting per-part transforms drive the same `MeshRefs` rebuild +that creature entities use (sequencer branch at `GameWindow.cs:7497`; doors +never enter the legacy slerp `else` branch). This is the reason the stance-value +bug produced underground doors: with the wrong style key (`0x80000001`) +`HasCycle` returned false, the sequencer was empty at spawn, `Advance` returned +identity frames, and the per-frame part-matrix rebuild received `Vector3.Zero / +Quaternion.Identity` for every part — collapsing them all to the entity origin. --- @@ -244,8 +252,9 @@ 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. +can be extended: broaden the detection predicate beyond `name == "Door"` and +register additional entity types in the existing `_animatedEntities` dict via +the same sibling branch. ### Door toggle behavior