Unify the toolbar shortcut manager with Runtime inventory state, route retail-ordered shortcut and spellbook command effects through the canonical owners, and make retained controllers borrow those exact instances. Remove the item-interaction transaction fallback and add graphical/no-window parity plus failure-safe terminal ownership-ledger coverage. Co-authored-by: Codex <codex@openai.com>
229 lines
7.2 KiB
C#
229 lines
7.2 KiB
C#
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send and apply one retail toolbar add on the canonical shortcut
|
|
/// manager. Retail emits <c>Event_AddShortCut</c> before mutating
|
|
/// <c>PlayerModule</c>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send and apply one retail toolbar removal on the canonical shortcut
|
|
/// manager.
|
|
/// </summary>
|
|
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<Exception>? 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<Exception>? 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<ShortcutEntry> 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);
|
|
}
|
|
}
|