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);
}
}