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.