From 9053860f1b94cafa3af1e55c46e020d3f9b39c42 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 14 May 2026 06:55:42 +0200 Subject: [PATCH] feat(B.4c): door spawn-time AnimationSequencer with state-seeded initial cycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds IsDoorSpawn helper and a sibling branch to the live-spawn handler's animation registration gate. Detects entities where spawn.Name == "Door" and registers them in _animatedEntities with an AnimationSequencer seeded from the spawn PhysicsState's ETHEREAL bit (Off cycle if closed, On if already open). Mirrors ACE Door.cs:43 (CurrentMotionState = motionClosed) so the sequencer always has frames for the per-frame tick to advance from the first render. Without the seed, Advance(dt) returns no frames and the MeshRefs rebuild at line 7691 collapses the door to origin. No changes to OnLiveMotionUpdated, AnimationSequencer, EntitySpawnAdapter, or the per-frame tick. The tick's sequencer branch at line 7497 reads ae.Sequencer.Advance(dt) and never touches ae.Animation in this path (only the legacy slerp else branch at line 7644+ does). [door-anim] registered diagnostic gated on ACDREAM_PROBE_BUILDING. One spec deviation: Animation = null! (null-forgiving) instead of Animation = null — AnimatedEntity.Animation is a required non-nullable field; null! is the same pattern used at line 7857 for sequencer-driven AnimatedEntity registrations in the same file. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 8e0fa7b..2b673bc 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -2127,6 +2127,14 @@ public sealed class GameWindow : IDisposable } } + /// + /// Phase B.4c — door detection by server-sent name. Doors fail the + /// generic multi-frame-idle gate at line 2692 (no idle cycle), so we + /// register them via a sibling branch with a state-seeded sequencer. + /// + private static bool IsDoorSpawn(AcDream.Core.Net.WorldSession.EntitySpawn spawn) + => spawn.Name == "Door"; + private void OnLiveEntitySpawnedLocked(AcDream.Core.Net.WorldSession.EntitySpawn spawn) { _liveSpawnReceived++; @@ -2798,6 +2806,61 @@ public sealed class GameWindow : IDisposable _entitySoundTables.Set(entity.Id, soundTableId); } } + else if (IsDoorSpawn(spawn) && _animLoader is not null) + { + // Phase B.4c — Door swing animation. Doors fail the + // multi-frame-idle gate above (no idle cycle) but DO have a + // MotionTable with On/Off cycles that ACE drives via + // UpdateMotion. Register with a seeded sequencer so the + // per-frame tick has frames to advance from frame 1 (without + // the seed, Sequencer.Advance(dt) returns no frames and the + // MeshRefs rebuild at line 7691 collapses the door to origin). + // + // Initial cycle mirrors ACE's Door.cs:43 + // (CurrentMotionState = motionClosed): Off when the door is + // closed at spawn, On when the spawn PhysicsState carries the + // ETHEREAL bit (door was already open in ACE's DB). + uint mtableId = spawn.MotionTableId ?? (uint)setup.DefaultMotionTable; + if (mtableId != 0) + { + var mtable = _dats.Get(mtableId); + if (mtable is not null) + { + var sequencer = new AcDream.Core.Physics.AnimationSequencer(setup, mtable, _animLoader); + + const uint NonCombatStance = 0x80000001u; + const uint MotionOn = 0x4000000Bu; // ACE MotionCommand.On (door open) + const uint MotionOff = 0x4000000Cu; // ACE MotionCommand.Off (door closed) + const uint EtherealPs = 0x4u; + uint spawnState = spawn.PhysicsState ?? 0u; + uint initialCycle = (spawnState & EtherealPs) != 0 ? MotionOn : MotionOff; + if (sequencer.HasCycle(NonCombatStance, initialCycle)) + sequencer.SetCycle(NonCombatStance, initialCycle); + + var template = new (uint, IReadOnlyDictionary?)[meshRefs.Count]; + for (int i = 0; i < meshRefs.Count; i++) + template[i] = (meshRefs[i].GfxObjId, meshRefs[i].SurfaceOverrides); + + _animatedEntities[entity.Id] = new AnimatedEntity + { + Entity = entity, + Setup = setup, + Animation = null!, // sequencer-driven; tick reads sequencer state + LowFrame = 0, + HighFrame = 0, + Framerate = 0f, + Scale = scale, + PartTemplate = template, + CurrFrame = 0, + Sequencer = sequencer, + }; + + if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeBuildingEnabled) + Console.WriteLine(System.FormattableString.Invariant( + $"[door-anim] registered guid=0x{spawn.Guid:X8} entityId=0x{entity.Id:X8} mtable=0x{mtableId:X8} initialCycle=0x{initialCycle:X8}")); + } + } + } // Dump a summary periodically so we can see drop breakdowns without // waiting for a graceful shutdown.