refactor(runtime): close canonical gameplay ownership
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>
This commit is contained in:
parent
ce6fae7b38
commit
89e6b207f8
36 changed files with 1433 additions and 251 deletions
|
|
@ -3,6 +3,33 @@ 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
|
||||
|
|
@ -19,7 +46,7 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
?? throw new ArgumentNullException(nameof(entityObjects));
|
||||
ExternalContainers = new ExternalContainerState();
|
||||
ItemMana = new ItemManaState();
|
||||
Shortcuts = new ShortcutState();
|
||||
Shortcuts = new ShortcutStore();
|
||||
Transactions = new InventoryTransactionState(_entityObjects.Objects);
|
||||
View = new InventoryStateView(this);
|
||||
}
|
||||
|
|
@ -27,11 +54,25 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
public ClientObjectTable Objects => _entityObjects.Objects;
|
||||
public ExternalContainerState ExternalContainers { get; }
|
||||
public ItemManaState ItemMana { get; }
|
||||
public ShortcutState Shortcuts { 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();
|
||||
|
|
@ -41,20 +82,82 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
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(() => ExternalContainers.Reset(), ref failures);
|
||||
Try(ItemMana.Clear, ref failures);
|
||||
Try(Shortcuts.Clear, ref failures);
|
||||
Try(Transactions.Dispose, ref failures);
|
||||
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);
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private static void Try(Action action, ref List<Exception>? failures)
|
||||
|
|
@ -93,7 +196,7 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
owner.Transactions.BusyCount,
|
||||
owner.Transactions.CanBeginRequest,
|
||||
pending,
|
||||
owner.Shortcuts.Items.Count,
|
||||
owner.Shortcuts.Count,
|
||||
owner.Shortcuts.Revision,
|
||||
owner.ItemMana.Count,
|
||||
owner.ItemMana.Revision);
|
||||
|
|
@ -124,22 +227,3 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
owner.ItemMana.TryGetManaPercent(objectId, out fraction);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ShortcutState
|
||||
{
|
||||
private IReadOnlyList<ShortcutEntry> _items = Array.Empty<ShortcutEntry>();
|
||||
private long _revision;
|
||||
|
||||
public IReadOnlyList<ShortcutEntry> Items => Volatile.Read(ref _items);
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
|
||||
public void Replace(IReadOnlyList<ShortcutEntry>? items)
|
||||
{
|
||||
Volatile.Write(
|
||||
ref _items,
|
||||
items ?? Array.Empty<ShortcutEntry>());
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
public void Clear() => Replace(Array.Empty<ShortcutEntry>());
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue