using System; namespace AcDream.Core.Items; public enum ExternalContainerTransitionKind { Opened, ReplacementRequested, Closed, Reset, } public readonly record struct ExternalContainerTransition( ExternalContainerTransitionKind Kind, uint PreviousContainerId, uint ContainerId); /// /// Owns retail ClientUISystem::groundObject and the outstanding external /// container request. ACE sends ViewContents for the requested root and then for /// nested containers; only the expected root may become the visible ground object. /// Retail: ClientUISystem::OnViewContents @ 0x00565870 and /// SetGroundObject @ 0x00564510. /// public sealed class ExternalContainerState { public uint RequestedContainerId { get; private set; } public uint CurrentContainerId { get; private set; } public event Action? Changed; public bool RequestOpen(uint containerId) { if (containerId == 0u || RequestedContainerId == containerId) return false; uint previous = CurrentContainerId; RequestedContainerId = containerId; // ClientUISystem::SetGroundObject @ 0x00564510 retires the old // ground-object presentation synchronously when a different root is // requested. ViewContents for the new root arrives later. Keeping the // old root current during that interval lets its range watcher issue a // close Use, which ACE interprets as cancellation of the new root's // MoveTo chain. if (previous != 0u && previous != containerId) { CurrentContainerId = 0u; Changed?.Invoke(new ExternalContainerTransition( ExternalContainerTransitionKind.ReplacementRequested, previous, containerId)); } return true; } public bool ApplyViewContents(uint containerId) { if (containerId == 0u || containerId != RequestedContainerId) return false; uint previous = CurrentContainerId; CurrentContainerId = containerId; RequestedContainerId = containerId; if (previous == containerId) return false; Changed?.Invoke(new ExternalContainerTransition( ExternalContainerTransitionKind.Opened, previous, containerId)); return true; } public bool ApplyClose(uint containerId) { if (containerId == 0u || containerId != CurrentContainerId) return false; uint previous = CurrentContainerId; CurrentContainerId = 0u; RequestedContainerId = 0u; Changed?.Invoke(new ExternalContainerTransition( ExternalContainerTransitionKind.Closed, previous, 0u)); return true; } public bool ApplyUseDone(uint weenieError) { if (weenieError == 0u || RequestedContainerId == CurrentContainerId) return false; RequestedContainerId = CurrentContainerId; return true; } public bool Reset() { uint previous = CurrentContainerId; bool changed = previous != 0u || RequestedContainerId != 0u; CurrentContainerId = 0u; RequestedContainerId = 0u; var transition = new ExternalContainerTransition( ExternalContainerTransitionKind.Reset, previous, 0u); Action? listeners = Changed; if (listeners is not null) { List? failures = null; foreach (Action listener in listeners.GetInvocationList()) { try { listener(transition); } catch (Exception error) { (failures ??= []).Add(error); } } if (failures is not null) throw new AggregateException( "One or more external-container reset observers failed.", failures); } return changed; } }