refactor(runtime): own inventory transaction state
Move the retail one-request-at-a-time gate, shared use busy references, external-container state, item mana, shortcuts, and desired-component snapshots into one Runtime-owned graph over J3's exact ClientObjectTable. Retained UI and session routing now borrow that owner; reset and shutdown preserve the existing order while failure/reentrancy tests protect the transaction boundary. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
595d5e6b01
commit
011efbeaa7
18 changed files with 1337 additions and 406 deletions
|
|
@ -19,50 +19,6 @@ public readonly record struct PendingBackpackPlacement(
|
|||
int Placement,
|
||||
ClientObject? ItemIdentity);
|
||||
|
||||
public enum InventoryRequestKind
|
||||
{
|
||||
Pickup,
|
||||
PutInContainer,
|
||||
SplitToContainer,
|
||||
Merge,
|
||||
DropToWorld,
|
||||
SplitToWorld,
|
||||
Give,
|
||||
}
|
||||
|
||||
public readonly record struct PendingInventoryRequest(
|
||||
ulong Token,
|
||||
InventoryRequestKind Kind,
|
||||
uint ItemId,
|
||||
ClientObject? ItemIdentity,
|
||||
bool Dispatched);
|
||||
|
||||
/// <summary>
|
||||
/// Owns one retail UI busy reference while an accepted Use request crosses the
|
||||
/// local approach boundary. Dispatch transfers that reference to the matching
|
||||
/// authoritative UseDone; cancellation before dispatch releases it locally.
|
||||
/// </summary>
|
||||
public sealed class ItemUseRequestReservation
|
||||
{
|
||||
private readonly Action<bool> _resolve;
|
||||
private int _resolved;
|
||||
|
||||
internal ItemUseRequestReservation(Action<bool> resolve)
|
||||
=> _resolve = resolve ?? throw new ArgumentNullException(nameof(resolve));
|
||||
|
||||
public void MarkDispatched()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _resolved, 1) == 0)
|
||||
_resolve(true);
|
||||
}
|
||||
|
||||
public void CancelBeforeDispatch()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _resolved, 1) == 0)
|
||||
_resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared retail item interaction orchestrator. UI widgets route item clicks,
|
||||
/// target acquisition, and drag-out drops here instead of duplicating
|
||||
|
|
@ -105,17 +61,15 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private readonly Action<string>? _systemMessage;
|
||||
private readonly AutoWieldController _autoWield;
|
||||
private readonly Action<uint, ItemUseRequestReservation>? _requestUse;
|
||||
private readonly InventoryTransactionState _transactions;
|
||||
private readonly bool _ownsTransactions;
|
||||
|
||||
private long _lastUseMs = long.MinValue / 2;
|
||||
private uint _consumedPrimaryClickTarget;
|
||||
private long _consumedPrimaryClickMs = long.MinValue / 2;
|
||||
private int _busyCount;
|
||||
private uint _awaitingAppraisalId;
|
||||
private uint _currentAppraisalId;
|
||||
private ulong _nextInventoryRequestToken;
|
||||
private PendingBackpackPlacement? _pendingBackpackPlacement;
|
||||
private PendingInventoryRequest? _pendingInventoryRequest;
|
||||
private ulong _useReservationGeneration;
|
||||
private bool _disposed;
|
||||
|
||||
public ItemInteractionController(
|
||||
|
|
@ -149,7 +103,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
Action<uint>? requestExternalContainer = null,
|
||||
CombatState? combatState = null,
|
||||
Action<CombatMode>? sendChangeCombatMode = null,
|
||||
Action<uint, ItemUseRequestReservation>? requestUse = null)
|
||||
Action<uint, ItemUseRequestReservation>? requestUse = null,
|
||||
InventoryTransactionState? transactions = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
|
|
@ -180,21 +135,38 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_systemMessage = systemMessage;
|
||||
_requestUse = requestUse;
|
||||
_interactionState = interactionState ?? new InteractionState();
|
||||
_transactions = transactions ?? new InventoryTransactionState(_objects);
|
||||
_ownsTransactions = transactions is null;
|
||||
if (!ReferenceEquals(_transactions.Objects, _objects))
|
||||
{
|
||||
if (_ownsTransactions)
|
||||
_transactions.Dispose();
|
||||
throw new ArgumentException(
|
||||
"The inventory transaction owner must borrow the controller's exact object table.",
|
||||
nameof(transactions));
|
||||
}
|
||||
try
|
||||
{
|
||||
_autoWield = new AutoWieldController(
|
||||
_objects,
|
||||
_playerGuid,
|
||||
_sendWield,
|
||||
sendPutItemInContainer,
|
||||
_toast,
|
||||
_systemMessage,
|
||||
combatState,
|
||||
sendChangeCombatMode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (_ownsTransactions)
|
||||
_transactions.Dispose();
|
||||
throw;
|
||||
}
|
||||
_interactionState.Changed += OnInteractionModeChanged;
|
||||
_objects.ObjectMoved += OnInventoryObjectMoved;
|
||||
_objects.MoveRequestFailed += OnInventoryMoveFailed;
|
||||
_objects.ObjectRemoved += OnInventoryObjectRemoved;
|
||||
_objects.StackSizeUpdated += OnInventoryStackSizeUpdated;
|
||||
_objects.Cleared += OnInventoryObjectsCleared;
|
||||
_autoWield = new AutoWieldController(
|
||||
_objects,
|
||||
_playerGuid,
|
||||
_sendWield,
|
||||
sendPutItemInContainer,
|
||||
_toast,
|
||||
_systemMessage,
|
||||
combatState,
|
||||
sendChangeCombatMode);
|
||||
_transactions.StateChanged += OnTransactionStateChanged;
|
||||
_transactions.RequestCompleted += OnInventoryRequestCompleted;
|
||||
_transactions.ObjectTableCleared += OnInventoryObjectsCleared;
|
||||
}
|
||||
|
||||
public event Action? StateChanged;
|
||||
|
|
@ -223,14 +195,16 @@ public sealed class ItemInteractionController : IDisposable
|
|||
|
||||
public InteractionState InteractionState => _interactionState;
|
||||
|
||||
public int BusyCount => _busyCount;
|
||||
public int BusyCount => _transactions.BusyCount;
|
||||
public uint CurrentAppraisalId => _currentAppraisalId;
|
||||
|
||||
public bool CanMakeInventoryRequest =>
|
||||
BaseCanMakeInventoryRequest && _pendingInventoryRequest is null;
|
||||
BaseCanMakeInventoryRequest && !_transactions.HasPendingRequest;
|
||||
|
||||
private bool BaseCanMakeInventoryRequest =>
|
||||
_readyForInventoryRequest() && _busyCount == 0 && !_autoWield.IsBusy;
|
||||
_readyForInventoryRequest()
|
||||
&& _transactions.BusyCount == 0
|
||||
&& !_autoWield.IsBusy;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>ACCWeenieObject::IsPlayerReadyToMakeInventoryRequest</c>.
|
||||
|
|
@ -242,8 +216,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
if (CanMakeInventoryRequest)
|
||||
return true;
|
||||
|
||||
if (_pendingInventoryRequest is not null
|
||||
|| _busyCount != 0
|
||||
if (_transactions.HasPendingRequest
|
||||
|| _transactions.BusyCount != 0
|
||||
|| _autoWield.IsBusy)
|
||||
_systemMessage?.Invoke(InventoryRequestBusyMessage);
|
||||
return false;
|
||||
|
|
@ -283,94 +257,28 @@ public sealed class ItemInteractionController : IDisposable
|
|||
|
||||
if (reservationToken != 0u)
|
||||
{
|
||||
if (_pendingInventoryRequest is not { } reserved
|
||||
|| reserved.Token != reservationToken
|
||||
|| reserved.ItemId != itemId
|
||||
|| reserved.Kind != kind
|
||||
|| reserved.Dispatched)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool reservedDispatch;
|
||||
try
|
||||
{
|
||||
reservedDispatch = dispatch();
|
||||
return _transactions.TryDispatch(
|
||||
kind,
|
||||
itemId,
|
||||
dispatch,
|
||||
reservationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
CancelPendingBackpackPlacement(itemId, reservationToken);
|
||||
throw;
|
||||
}
|
||||
if (!reservedDispatch)
|
||||
{
|
||||
CancelPendingBackpackPlacement(itemId, reservationToken);
|
||||
return false;
|
||||
}
|
||||
// A synchronous test transport or reentrant table listener may
|
||||
// already have completed this exact request. Never resurrect it or
|
||||
// overwrite a newer transaction acquired from that response.
|
||||
if (_pendingInventoryRequest is { } reservationCurrent
|
||||
&& reservationCurrent.Token == reserved.Token)
|
||||
{
|
||||
_pendingInventoryRequest = reserved with { Dispatched = true };
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
|
||||
ulong token = ++_nextInventoryRequestToken;
|
||||
if (token == 0u)
|
||||
token = ++_nextInventoryRequestToken;
|
||||
var provisional = new PendingInventoryRequest(
|
||||
token,
|
||||
kind,
|
||||
itemId,
|
||||
_objects.Get(itemId),
|
||||
Dispatched: false);
|
||||
// Provisional ownership closes the reentrancy window around optimistic
|
||||
// table mutation and arbitrary send adapters. Retail writes prevRequest
|
||||
// after its synchronous event call; our callback seam can publish local
|
||||
// notices reentrantly, so the owner is reserved before entering it.
|
||||
_pendingInventoryRequest = provisional;
|
||||
StateChanged?.Invoke();
|
||||
|
||||
bool dispatched;
|
||||
try
|
||||
{
|
||||
dispatched = dispatch();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ClearPendingInventoryRequest(token);
|
||||
throw;
|
||||
}
|
||||
if (!dispatched)
|
||||
{
|
||||
ClearPendingInventoryRequest(token);
|
||||
return false;
|
||||
}
|
||||
if (_pendingInventoryRequest is { } current
|
||||
&& current.Token == token)
|
||||
{
|
||||
_pendingInventoryRequest = provisional with { Dispatched = true };
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
return true;
|
||||
return _transactions.TryDispatch(kind, itemId, dispatch);
|
||||
}
|
||||
|
||||
public bool TryGetPendingInventoryRequest(out PendingInventoryRequest pending)
|
||||
{
|
||||
if (_pendingInventoryRequest is { } current)
|
||||
{
|
||||
pending = current;
|
||||
return true;
|
||||
}
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
=> _transactions.TryGetPending(out pending);
|
||||
|
||||
/// <summary>
|
||||
/// Increments retail's shared <c>ClientUISystem</c> busy reference after a
|
||||
|
|
@ -379,10 +287,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
/// Item__UseDone too; AttackDone belongs only to the combat attack owner.
|
||||
/// </summary>
|
||||
public void IncrementBusyCount()
|
||||
{
|
||||
_busyCount++;
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
=> _transactions.IncrementBusyCount();
|
||||
|
||||
/// <summary>
|
||||
/// Raised for retail confirmation/auxiliary actions whose retained panel is
|
||||
|
|
@ -530,10 +435,11 @@ public sealed class ItemInteractionController : IDisposable
|
|||
if (firstResponse)
|
||||
{
|
||||
_awaitingAppraisalId = 0;
|
||||
if (_busyCount > 0)
|
||||
_busyCount--;
|
||||
_currentAppraisalId = objectId;
|
||||
StateChanged?.Invoke();
|
||||
bool ownedBusyReference = _transactions.BusyCount > 0;
|
||||
_transactions.CompleteUse(0u);
|
||||
if (!ownedBusyReference)
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
return new AppraisalResponseAcceptance(
|
||||
|
|
@ -570,12 +476,18 @@ public sealed class ItemInteractionController : IDisposable
|
|||
if (_awaitingAppraisalId != 0u)
|
||||
{
|
||||
_awaitingAppraisalId = 0u;
|
||||
if (_busyCount > 0)
|
||||
_busyCount--;
|
||||
_currentAppraisalId = 0u;
|
||||
bool ownedBusyReference = _transactions.BusyCount > 0;
|
||||
_transactions.CompleteUse(0u);
|
||||
if (!ownedBusyReference)
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentAppraisalId = 0u;
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
_currentAppraisalId = 0u;
|
||||
_sendExamine?.Invoke(0u);
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -694,34 +606,63 @@ public sealed class ItemInteractionController : IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
ulong token = ++_nextInventoryRequestToken;
|
||||
if (token == 0u)
|
||||
token = ++_nextInventoryRequestToken;
|
||||
ClientObject? identity = _objects.Get(itemGuid);
|
||||
pending = new PendingBackpackPlacement(
|
||||
token,
|
||||
itemGuid,
|
||||
containerId,
|
||||
placement,
|
||||
identity);
|
||||
_pendingBackpackPlacement = pending;
|
||||
_pendingInventoryRequest = new PendingInventoryRequest(
|
||||
token,
|
||||
kind,
|
||||
itemGuid,
|
||||
identity,
|
||||
Dispatched: false);
|
||||
PendingBackpackPlacementRequested?.Invoke(pending);
|
||||
if (_pendingBackpackPlacement != pending
|
||||
|| _pendingInventoryRequest is not { } published
|
||||
|| published.Token != token
|
||||
PendingBackpackPlacement candidate = default;
|
||||
bool reserved;
|
||||
PendingInventoryRequest published;
|
||||
try
|
||||
{
|
||||
reserved = _transactions.TryReserve(
|
||||
kind,
|
||||
itemGuid,
|
||||
out published,
|
||||
request =>
|
||||
{
|
||||
candidate = new PendingBackpackPlacement(
|
||||
request.Token,
|
||||
itemGuid,
|
||||
containerId,
|
||||
placement,
|
||||
request.ItemIdentity);
|
||||
_pendingBackpackPlacement = candidate;
|
||||
List<Exception> failures = [];
|
||||
DispatchAll(
|
||||
PendingBackpackPlacementRequested,
|
||||
candidate,
|
||||
failures);
|
||||
if (failures.Count != 0)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"One or more pending-placement observers failed.",
|
||||
failures);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
if (candidate.Token == 0u)
|
||||
throw;
|
||||
|
||||
_pendingBackpackPlacement = null;
|
||||
_transactions.CancelBeforeDispatch(candidate.Token);
|
||||
List<Exception> failures = [failure];
|
||||
DispatchAll(
|
||||
PendingBackpackPlacementCancelled,
|
||||
candidate,
|
||||
failures);
|
||||
throw new AggregateException(
|
||||
"Pending backpack placement publication failed.",
|
||||
failures);
|
||||
}
|
||||
pending = candidate;
|
||||
if (!reserved
|
||||
|| _pendingBackpackPlacement != candidate
|
||||
|| published.Token != candidate.Token
|
||||
|| published.ItemId != itemGuid
|
||||
|| published.Kind != kind
|
||||
|| published.Dispatched)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
StateChanged?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -753,7 +694,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
}
|
||||
|
||||
if (_pendingBackpackPlacement != pending
|
||||
|| _pendingInventoryRequest is not { } reserved
|
||||
|| !_transactions.TryGetPending(out PendingInventoryRequest reserved)
|
||||
|| reserved.Token != pending.Token
|
||||
|| reserved.ItemId != pending.ItemId
|
||||
|| reserved.Kind != kind
|
||||
|
|
@ -798,25 +739,15 @@ public sealed class ItemInteractionController : IDisposable
|
|||
}
|
||||
|
||||
_pendingBackpackPlacement = null;
|
||||
if (_pendingInventoryRequest is { } request
|
||||
&& request.Token == pending.Token
|
||||
&& !request.Dispatched)
|
||||
_transactions.CancelBeforeDispatch(pending.Token);
|
||||
List<Exception> failures = [];
|
||||
DispatchAll(PendingBackpackPlacementCancelled, pending, failures);
|
||||
if (failures.Count != 0)
|
||||
{
|
||||
_pendingInventoryRequest = null;
|
||||
StateChanged?.Invoke();
|
||||
throw new AggregateException(
|
||||
"One or more pending-placement cancellation observers failed.",
|
||||
failures);
|
||||
}
|
||||
PendingBackpackPlacementCancelled?.Invoke(pending);
|
||||
}
|
||||
|
||||
private void ClearPendingInventoryRequest(ulong token)
|
||||
{
|
||||
if (_pendingInventoryRequest is not { } pending
|
||||
|| pending.Token != token)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_pendingInventoryRequest = null;
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
internal bool TryCaptureObjectIdentity(uint objectId, out ClientObject item)
|
||||
|
|
@ -871,7 +802,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
_sendUseWithTarget?.Invoke(sourceGuid, targetGuid);
|
||||
_busyCount++;
|
||||
_transactions.IncrementBusyCount();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -906,7 +837,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
else
|
||||
{
|
||||
_sendUse!(objectId);
|
||||
_busyCount++;
|
||||
_transactions.IncrementBusyCount();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1023,7 +954,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
_busyCount++;
|
||||
_transactions.IncrementBusyCount();
|
||||
}
|
||||
break;
|
||||
case ItemPolicyActionKind.Reject:
|
||||
|
|
@ -1044,19 +975,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
}
|
||||
|
||||
private ItemUseRequestReservation BeginUseRequestReservation()
|
||||
{
|
||||
ulong generation = _useReservationGeneration;
|
||||
_busyCount++;
|
||||
StateChanged?.Invoke();
|
||||
return new ItemUseRequestReservation(dispatched =>
|
||||
{
|
||||
if (generation != _useReservationGeneration || dispatched)
|
||||
return;
|
||||
if (_busyCount > 0)
|
||||
_busyCount--;
|
||||
StateChanged?.Invoke();
|
||||
});
|
||||
}
|
||||
=> _transactions.BeginUseRequestReservation();
|
||||
|
||||
private void ExecutePlacementActions(System.Collections.Generic.IReadOnlyList<ItemPolicyAction> actions)
|
||||
{
|
||||
|
|
@ -1204,81 +1123,39 @@ public sealed class ItemInteractionController : IDisposable
|
|||
failures);
|
||||
}
|
||||
|
||||
private void OnInventoryObjectMoved(ClientObjectMove move)
|
||||
private void OnTransactionStateChanged()
|
||||
{
|
||||
if (move.Origin != ClientObjectMoveOrigin.AuthoritativeResponse)
|
||||
return;
|
||||
|
||||
CompleteInventoryResponse(move.ItemId, move.Item);
|
||||
}
|
||||
|
||||
private void OnInventoryMoveFailed(MoveRequestFailure failure)
|
||||
{
|
||||
ClientObject? identity = _objects.Get(failure.ItemId);
|
||||
CompleteInventoryResponse(failure.ItemId, identity);
|
||||
}
|
||||
|
||||
private void OnInventoryObjectRemoved(ClientObject item)
|
||||
{
|
||||
CompleteInventoryResponse(item.ObjectId, item);
|
||||
}
|
||||
|
||||
private void OnInventoryStackSizeUpdated(ClientObject item)
|
||||
{
|
||||
CompleteInventoryResponse(item.ObjectId, item);
|
||||
}
|
||||
|
||||
private void CompleteInventoryResponse(uint itemId, ClientObject? identity)
|
||||
{
|
||||
PendingInventoryRequest? completedRequest = null;
|
||||
PendingBackpackPlacement? completedPlacement = null;
|
||||
|
||||
if (_pendingInventoryRequest is { } request
|
||||
&& request.ItemId == itemId
|
||||
&& MatchesIdentity(request.ItemIdentity, itemId, identity))
|
||||
List<Exception> failures = [];
|
||||
DispatchAll(StateChanged, failures);
|
||||
if (failures.Count != 0)
|
||||
{
|
||||
completedRequest = request;
|
||||
_pendingInventoryRequest = null;
|
||||
throw new AggregateException(
|
||||
"One or more item-interaction observers failed.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInventoryRequestCompleted(PendingInventoryRequest request)
|
||||
{
|
||||
if (_pendingBackpackPlacement is { } placement
|
||||
&& placement.ItemId == itemId
|
||||
&& MatchesIdentity(placement.ItemIdentity, itemId, identity))
|
||||
&& placement.Token == request.Token
|
||||
&& placement.ItemId == request.ItemId)
|
||||
{
|
||||
completedPlacement = placement;
|
||||
_pendingBackpackPlacement = null;
|
||||
List<Exception> failures = [];
|
||||
DispatchAll(PendingBackpackPlacementResolved, placement, failures);
|
||||
if (failures.Count != 0)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"One or more pending-placement resolution observers failed.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
|
||||
// ACCWeenieObject::RecordResponse @ 0x0058CAB0 clears prevRequest
|
||||
// before ServerSaysMoveItem publishes the item-list notice. Clear the
|
||||
// coupled global owner and local waiting projection atomically before
|
||||
// either callback so observers never see a half-completed transaction.
|
||||
if (completedPlacement is { } resolved)
|
||||
PendingBackpackPlacementResolved?.Invoke(resolved);
|
||||
if (completedRequest is not null)
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
private bool MatchesIdentity(
|
||||
ClientObject? expected,
|
||||
uint itemId,
|
||||
ClientObject? actual)
|
||||
{
|
||||
if (expected is null)
|
||||
return true;
|
||||
if (actual is not null)
|
||||
return ReferenceEquals(expected, actual);
|
||||
return _objects.Get(itemId) is not { } current
|
||||
|| ReferenceEquals(expected, current);
|
||||
}
|
||||
|
||||
private void OnInventoryObjectsCleared()
|
||||
{
|
||||
bool changed = _pendingInventoryRequest is not null;
|
||||
_pendingInventoryRequest = null;
|
||||
_pendingBackpackPlacement = null;
|
||||
if (changed)
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -1286,30 +1163,21 @@ public sealed class ItemInteractionController : IDisposable
|
|||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_interactionState.Changed -= OnInteractionModeChanged;
|
||||
_objects.ObjectMoved -= OnInventoryObjectMoved;
|
||||
_objects.MoveRequestFailed -= OnInventoryMoveFailed;
|
||||
_objects.ObjectRemoved -= OnInventoryObjectRemoved;
|
||||
_objects.StackSizeUpdated -= OnInventoryStackSizeUpdated;
|
||||
_objects.Cleared -= OnInventoryObjectsCleared;
|
||||
_transactions.ObjectTableCleared -= OnInventoryObjectsCleared;
|
||||
_transactions.RequestCompleted -= OnInventoryRequestCompleted;
|
||||
_transactions.StateChanged -= OnTransactionStateChanged;
|
||||
if (_ownsTransactions)
|
||||
_transactions.Dispose();
|
||||
_autoWield.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Retail UseDone (0x01C7) releases one UI busy reference.</summary>
|
||||
public void CompleteUse(uint _)
|
||||
{
|
||||
if (_busyCount == 0) return;
|
||||
_busyCount--;
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
=> _transactions.CompleteUse(0u);
|
||||
|
||||
/// <summary>Session teardown drops every outstanding client UI busy reference.</summary>
|
||||
public void ClearBusy()
|
||||
{
|
||||
if (_busyCount == 0) return;
|
||||
_useReservationGeneration++;
|
||||
_busyCount = 0;
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
=> _transactions.ClearBusy();
|
||||
|
||||
/// <summary>
|
||||
/// Ends all session-scoped ItemHolder state. Target modes, click
|
||||
|
|
@ -1320,11 +1188,21 @@ public sealed class ItemInteractionController : IDisposable
|
|||
{
|
||||
PendingBackpackPlacement? pendingPlacement = _pendingBackpackPlacement;
|
||||
_pendingBackpackPlacement = null;
|
||||
_useReservationGeneration++;
|
||||
_busyCount = 0;
|
||||
// The canonical owner publishes its reset to Runtime observers, while
|
||||
// this controller preserves the pre-J4 single UI notification emitted
|
||||
// by InteractionState.ResetSession below.
|
||||
_transactions.StateChanged -= OnTransactionStateChanged;
|
||||
try
|
||||
{
|
||||
_transactions.ResetSession();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!_disposed)
|
||||
_transactions.StateChanged += OnTransactionStateChanged;
|
||||
}
|
||||
_awaitingAppraisalId = 0;
|
||||
_currentAppraisalId = 0;
|
||||
_pendingInventoryRequest = null;
|
||||
_lastUseMs = long.MinValue / 2;
|
||||
_consumedPrimaryClickTarget = 0u;
|
||||
_consumedPrimaryClickMs = long.MinValue / 2;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue