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
|
|
@ -42,6 +42,15 @@ public sealed class ChatCommandTargetState : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public bool IsDisposed
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
return _disposed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forget character-scoped command targets without clearing visible
|
||||
/// transcript history.
|
||||
|
|
|
|||
|
|
@ -9,10 +9,40 @@ namespace AcDream.Core.Items;
|
|||
/// <see cref="ShortcutEntry"/> fields, including non-object records that the object-only
|
||||
/// <c>gmToolbarUI</c> does not render.
|
||||
/// </summary>
|
||||
public sealed class ShortcutStore
|
||||
public sealed class ShortcutStore : IDisposable
|
||||
{
|
||||
public const int SlotCount = 18;
|
||||
private readonly ShortcutEntry?[] _entries = new ShortcutEntry?[SlotCount];
|
||||
private ShortcutEntry[] _items = [];
|
||||
private Action? _changed;
|
||||
private long _revision;
|
||||
private long _dispatchFailureCount;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Raised synchronously after the canonical slot map changes. One failing
|
||||
/// observer cannot prevent another host from seeing the same mutation.
|
||||
/// </summary>
|
||||
public event Action? Changed
|
||||
{
|
||||
add
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
_changed += value;
|
||||
}
|
||||
remove => _changed -= value;
|
||||
}
|
||||
|
||||
/// <summary>Stable packed-order view of all present shortcut records.</summary>
|
||||
public IReadOnlyList<ShortcutEntry> Items => Volatile.Read(ref _items);
|
||||
|
||||
public int Count => Volatile.Read(ref _items).Length;
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
public int SubscriberCount => _changed?.GetInvocationList().Length ?? 0;
|
||||
public long DispatchFailureCount =>
|
||||
Interlocked.Read(ref _dispatchFailureCount);
|
||||
public Exception? LastDispatchFailure { get; private set; }
|
||||
public bool IsDisposed => _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Replace all slots from packed entries. Invalid signed indices are rejected exactly
|
||||
|
|
@ -21,9 +51,12 @@ public sealed class ShortcutStore
|
|||
/// </summary>
|
||||
public void Load(IEnumerable<ShortcutEntry> entries)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(entries);
|
||||
Array.Clear(_entries);
|
||||
foreach (var entry in entries)
|
||||
Set(entry);
|
||||
SetCore(entry);
|
||||
PublishChanged();
|
||||
}
|
||||
|
||||
/// <summary>Raw entry at <paramref name="slot"/>, or null for absent/out-of-range.</summary>
|
||||
|
|
@ -44,8 +77,9 @@ public sealed class ShortcutStore
|
|||
|
||||
public void Set(ShortcutEntry entry)
|
||||
{
|
||||
if ((uint)entry.Index < SlotCount)
|
||||
_entries[entry.Index] = entry;
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (SetCore(entry))
|
||||
PublishChanged();
|
||||
}
|
||||
|
||||
/// <summary>Create/replace a retail object shortcut; toolbar-created records use spell word zero.</summary>
|
||||
|
|
@ -53,7 +87,79 @@ public sealed class ShortcutStore
|
|||
|
||||
public void Remove(int slot)
|
||||
{
|
||||
if ((uint)slot < SlotCount)
|
||||
_entries[slot] = null;
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if ((uint)slot >= SlotCount || _entries[slot] is null)
|
||||
return;
|
||||
_entries[slot] = null;
|
||||
PublishChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the complete retail shortcut manager while retaining this owner
|
||||
/// for the next session.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
Array.Clear(_entries);
|
||||
PublishChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
Array.Clear(_entries);
|
||||
RebuildItems();
|
||||
Interlocked.Increment(ref _revision);
|
||||
DispatchChanged();
|
||||
_changed = null;
|
||||
}
|
||||
|
||||
private bool SetCore(ShortcutEntry entry)
|
||||
{
|
||||
if ((uint)entry.Index >= SlotCount
|
||||
|| _entries[entry.Index] == entry)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_entries[entry.Index] = entry;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void PublishChanged()
|
||||
{
|
||||
RebuildItems();
|
||||
Interlocked.Increment(ref _revision);
|
||||
DispatchChanged();
|
||||
}
|
||||
|
||||
private void RebuildItems()
|
||||
{
|
||||
var items = new List<ShortcutEntry>(SlotCount);
|
||||
for (int i = 0; i < _entries.Length; i++)
|
||||
{
|
||||
if (_entries[i] is ShortcutEntry entry)
|
||||
items.Add(entry);
|
||||
}
|
||||
Volatile.Write(ref _items, items.ToArray());
|
||||
}
|
||||
|
||||
private void DispatchChanged()
|
||||
{
|
||||
Delegate[] observers = _changed?.GetInvocationList() ?? [];
|
||||
foreach (Delegate observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
((Action)observer)();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
Interlocked.Increment(ref _dispatchFailureCount);
|
||||
LastDispatchFailure = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -401,11 +401,25 @@ public sealed class Spellbook
|
|||
|
||||
public void SetDesiredComponent(uint componentId, uint amount)
|
||||
{
|
||||
uint current = _desiredComponents.TryGetValue(componentId, out uint existing)
|
||||
? existing : 0u;
|
||||
if (current == amount) return;
|
||||
if (amount == 0) _desiredComponents.Remove(componentId);
|
||||
else _desiredComponents[componentId] = amount;
|
||||
bool exists = _desiredComponents.TryGetValue(
|
||||
componentId,
|
||||
out uint current);
|
||||
if (exists && current == amount) return;
|
||||
// PlayerModule::SetDesiredCompLevel @ 0x005D4940 retains zero as a
|
||||
// real table value; only ClearDesiredCompList destroys the entry set.
|
||||
_desiredComponents[componentId] = amount;
|
||||
DesiredComponentsChanged?.Invoke();
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroy the complete desired-component list, matching
|
||||
/// <c>PlayerModule::ClearDesiredCompList @ 0x005D2A00</c>.
|
||||
/// </summary>
|
||||
public void ClearDesiredComponents()
|
||||
{
|
||||
if (_desiredComponents.Count == 0) return;
|
||||
_desiredComponents.Clear();
|
||||
DesiredComponentsChanged?.Invoke();
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue