using AcDream.Core.Items; using AcDream.Runtime.Entities; namespace AcDream.Runtime.Gameplay; public readonly record struct RuntimeInventoryOwnershipSnapshot( bool IsDisposed, bool TransactionsDisposed, bool ShortcutsDisposed, int BusyCount, bool HasPendingRequest, uint RequestedContainerId, uint CurrentContainerId, int ItemManaCount, int ShortcutCount, int ShortcutSubscriberCount, long ShortcutDispatchFailureCount, long TransactionDispatchFailureCount) { public bool IsConverged => IsDisposed && TransactionsDisposed && ShortcutsDisposed && BusyCount == 0 && !HasPendingRequest && RequestedContainerId == 0u && CurrentContainerId == 0u && ItemManaCount == 0 && ShortcutCount == 0 && ShortcutSubscriberCount == 0; } /// /// Canonical presentation-independent owner for inventory gameplay state. /// The object collection is borrowed directly from J3's entity/object /// lifetime; this owner creates no second inventory model. /// public sealed class RuntimeInventoryState : IDisposable { private readonly RuntimeEntityObjectLifetime _entityObjects; private bool _disposed; public RuntimeInventoryState(RuntimeEntityObjectLifetime entityObjects) { _entityObjects = entityObjects ?? throw new ArgumentNullException(nameof(entityObjects)); ExternalContainers = new ExternalContainerState(); ItemMana = new ItemManaState(); Shortcuts = new ShortcutStore(); Transactions = new InventoryTransactionState(_entityObjects.Objects); View = new InventoryStateView(this); } public ClientObjectTable Objects => _entityObjects.Objects; public ExternalContainerState ExternalContainers { get; } public ItemManaState ItemMana { get; } public ShortcutStore Shortcuts { get; } public InventoryTransactionState Transactions { get; } public IRuntimeInventoryStateView View { get; } public bool IsDisposed => _disposed; public RuntimeInventoryOwnershipSnapshot CaptureOwnership() => new( _disposed, Transactions.IsDisposed, Shortcuts.IsDisposed, Transactions.BusyCount, Transactions.HasPendingRequest, ExternalContainers.RequestedContainerId, ExternalContainers.CurrentContainerId, ItemMana.Count, Shortcuts.Count, Shortcuts.SubscriberCount, Shortcuts.DispatchFailureCount, Transactions.DispatchFailureCount); public void ResetExternalContainer() => ExternalContainers.Reset(); public void ResetTransactions() => Transactions.ResetSession(); public void ResetItemMana() => ItemMana.Clear(); public void ResetPlayerSnapshots() { Shortcuts.Clear(); } /// /// Send and apply one retail toolbar add on the canonical shortcut /// manager. Retail emits Event_AddShortCut before mutating /// PlayerModule. /// public bool TryAddShortcut( ShortcutEntry entry, Action publishOutbound) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(publishOutbound); if ((uint)entry.Index >= ShortcutStore.SlotCount || (entry.ObjectId == 0u && entry.SpellId == 0u)) { return false; } try { publishOutbound(); } finally { Shortcuts.Set(entry); } return true; } /// /// Send and apply one retail toolbar removal on the canonical shortcut /// manager. /// public bool TryRemoveShortcut( int index, Action publishOutbound) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(publishOutbound); if ((uint)index >= ShortcutStore.SlotCount) return false; try { publishOutbound(); } finally { Shortcuts.Remove(index); } return true; } public void Dispose() { if (_disposed) return; List? failures = null; try { Try(() => ExternalContainers.Reset(), ref failures); Try(ItemMana.Clear, ref failures); Try(Shortcuts.Dispose, ref failures); Try(Transactions.Dispose, ref failures); } finally { // Disposal is terminal even when a borrowed presentation observer // fails. Every owned leaf above mutates before dispatching, so the // full suffix is safe to run and the Runtime root must never remain // half-alive over already-disposed children. _disposed = true; } if (failures is not null) throw new AggregateException( "Runtime inventory state did not converge during disposal.", failures); } private static void Try(Action action, ref List? failures) { try { action(); } catch (Exception error) { (failures ??= []).Add(error); } } private sealed class InventoryStateView(RuntimeInventoryState owner) : IRuntimeInventoryStateView { public RuntimeInventoryStateSnapshot Snapshot { get { RuntimePendingInventoryRequestSnapshot? pending = null; if (owner.Transactions.TryGetPending( out PendingInventoryRequest request)) { pending = new RuntimePendingInventoryRequestSnapshot( request.Token, (int)request.Kind, request.ItemId, request.Dispatched); } return new RuntimeInventoryStateSnapshot( owner.ExternalContainers.RequestedContainerId, owner.ExternalContainers.CurrentContainerId, owner.Transactions.BusyCount, owner.Transactions.CanBeginRequest, pending, owner.Shortcuts.Count, owner.Shortcuts.Revision, owner.ItemMana.Count, owner.ItemMana.Revision); } } public bool TryGetShortcut( int index, out RuntimeShortcutSnapshot shortcut) { IReadOnlyList shortcuts = owner.Shortcuts.Items; for (int i = 0; i < shortcuts.Count; i++) { if (shortcuts[i].Index != index) continue; ShortcutEntry current = shortcuts[i]; shortcut = new RuntimeShortcutSnapshot( current.Index, current.ObjectId, current.SpellId); return true; } shortcut = default; return false; } public bool TryGetItemMana(uint objectId, out float fraction) => owner.ItemMana.TryGetManaPercent(objectId, out fraction); } }