using AcDream.Core.Items; namespace AcDream.App.World; /// /// Owns the network side effect of replacing retail's current ground object. /// Presentation controllers observe the same state but do not own session /// lifecycle messages. /// public sealed class ExternalContainerLifecycleController : IDisposable { private readonly ExternalContainerState _state; private readonly ClientObjectTable _objects; private readonly Action _sendNoLongerViewingContents; private bool _disposed; public ExternalContainerLifecycleController( ExternalContainerState state, ClientObjectTable objects, Action sendNoLongerViewingContents) { _state = state ?? throw new ArgumentNullException(nameof(state)); _objects = objects ?? throw new ArgumentNullException(nameof(objects)); _sendNoLongerViewingContents = sendNoLongerViewingContents ?? throw new ArgumentNullException(nameof(sendNoLongerViewingContents)); _state.Changed += OnChanged; } private void OnChanged(ExternalContainerTransition transition) { // ClientUISystem::SetGroundObject @ 0x00564510 always queues the old // root's contents tree for destruction when the ground object changes. if (transition.PreviousContainerId != 0u) _objects.StopViewingContentsTree(transition.PreviousContainerId); // SetGroundObject(new, notifyServer:true) emits this while the new // request is still pending, not after ViewContents arrives. The // authored close/range path instead sends Use(root) and waits for // event 0x0052. if (transition.Kind == ExternalContainerTransitionKind.ReplacementRequested && transition.PreviousContainerId != 0u) { _sendNoLongerViewingContents(transition.PreviousContainerId); } } public void Dispose() { if (_disposed) return; _disposed = true; _state.Changed -= OnChanged; } }