refactor(runtime): own interaction transactions

Move use throttling, appraisal identity, queued interactions, and exact post-arrival pickup state beneath RuntimeActionState. Keep App as the picker, movement, transport, and retained-presentation adapter while preserving retail send and UseDone ordering. Add reset, disposal, GUID-reuse, callback-reentrancy, and transport-failure coverage.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-26 11:13:09 +02:00
parent be73bccf5a
commit f5f7b4177f
31 changed files with 1365 additions and 432 deletions

View file

@ -27,7 +27,6 @@ public readonly record struct PendingBackpackPlacement(
/// </summary>
public sealed class ItemInteractionController : IDisposable
{
private const long RetailUseThrottleMs = 200;
internal const string InventoryRequestBusyMessage =
"You can only move or use one item at a time";
private const long RetailDoubleClickMs = 500;
@ -62,19 +61,17 @@ public sealed class ItemInteractionController : IDisposable
private readonly Action<string>? _systemMessage;
private readonly AutoWieldController _autoWield;
private readonly Action<uint, ItemUseRequestReservation>? _requestUse;
private readonly RuntimeInteractionTransactionState _runtimeTransactions;
private readonly InventoryTransactionState _transactions;
private long _lastUseMs = long.MinValue / 2;
private uint _consumedPrimaryClickTarget;
private long _consumedPrimaryClickMs = long.MinValue / 2;
private uint _awaitingAppraisalId;
private uint _currentAppraisalId;
private PendingBackpackPlacement? _pendingBackpackPlacement;
private bool _disposed;
public ItemInteractionController(
ClientObjectTable objects,
InventoryTransactionState transactions,
RuntimeInteractionTransactionState runtimeTransactions,
InteractionState interactionState,
Func<uint> playerGuid,
Action<uint>? sendUse,
@ -136,13 +133,14 @@ public sealed class ItemInteractionController : IDisposable
_requestUse = requestUse;
_interactionState = interactionState
?? throw new ArgumentNullException(nameof(interactionState));
_transactions = transactions
?? throw new ArgumentNullException(nameof(transactions));
_runtimeTransactions = runtimeTransactions
?? throw new ArgumentNullException(nameof(runtimeTransactions));
_transactions = _runtimeTransactions.Inventory;
if (!ReferenceEquals(_transactions.Objects, _objects))
{
throw new ArgumentException(
"The inventory transaction owner must borrow the controller's exact object table.",
nameof(transactions));
nameof(runtimeTransactions));
}
_autoWield = new AutoWieldController(
_objects,
@ -184,9 +182,12 @@ public sealed class ItemInteractionController : IDisposable
public uint PlayerGuid => _playerGuid();
public InteractionState InteractionState => _interactionState;
public RuntimeInteractionTransactionState RuntimeTransactions =>
_runtimeTransactions;
public int BusyCount => _transactions.BusyCount;
public uint CurrentAppraisalId => _currentAppraisalId;
public uint CurrentAppraisalId =>
_runtimeTransactions.CurrentAppraisalId;
public bool CanMakeInventoryRequest =>
BaseCanMakeInventoryRequest && !_transactions.HasPendingRequest;
@ -277,7 +278,7 @@ public sealed class ItemInteractionController : IDisposable
/// Item__UseDone too; AttackDone belongs only to the combat attack owner.
/// </summary>
public void IncrementBusyCount()
=> _transactions.IncrementBusyCount();
=> _runtimeTransactions.IncrementBusyCount();
/// <summary>
/// Raised for retail confirmation/auxiliary actions whose retained panel is
@ -403,11 +404,7 @@ public sealed class ItemInteractionController : IDisposable
{
if (objectId == 0 || _sendExamine is null)
return;
if (_awaitingAppraisalId == 0)
IncrementBusyCount();
_awaitingAppraisalId = objectId;
_sendExamine(objectId);
_runtimeTransactions.TryRequestAppraisal(objectId, _sendExamine);
}
/// <summary>
@ -416,25 +413,14 @@ public sealed class ItemInteractionController : IDisposable
/// </summary>
public AppraisalResponseAcceptance AcceptAppraisalResponse(uint objectId)
{
if (objectId == 0
|| (objectId != _awaitingAppraisalId
&& objectId != _currentAppraisalId))
return default;
bool firstResponse = objectId == _awaitingAppraisalId;
if (firstResponse)
{
_awaitingAppraisalId = 0;
_currentAppraisalId = objectId;
bool ownedBusyReference = _transactions.BusyCount > 0;
_transactions.CompleteUse(0u);
if (!ownedBusyReference)
StateChanged?.Invoke();
}
bool ownedBusyReference = _transactions.BusyCount > 0;
RuntimeAppraisalResponseAcceptance acceptance =
_runtimeTransactions.AcceptAppraisalResponse(objectId);
if (acceptance.FirstResponse && !ownedBusyReference)
StateChanged?.Invoke();
return new AppraisalResponseAcceptance(
Accepted: true,
FirstResponse: firstResponse);
acceptance.Accepted,
acceptance.FirstResponse);
}
/// <summary>
@ -443,10 +429,9 @@ public sealed class ItemInteractionController : IDisposable
/// </summary>
public bool RefreshCurrentAppraisal()
{
if (_currentAppraisalId == 0 || _sendExamine is null)
if (_sendExamine is null)
return false;
_sendExamine(_currentAppraisalId);
return true;
return _runtimeTransactions.RefreshCurrentAppraisal(_sendExamine);
}
/// <summary>
@ -460,24 +445,14 @@ public sealed class ItemInteractionController : IDisposable
/// </summary>
public void CancelObjectAppraisalForSpell()
{
if (_awaitingAppraisalId == 0u && _currentAppraisalId == 0u)
if (_sendExamine is null)
return;
if (_awaitingAppraisalId != 0u)
bool ownedBusyReference = _transactions.BusyCount > 0;
if (_runtimeTransactions.CancelObjectAppraisalForSpell(_sendExamine)
&& !ownedBusyReference)
{
_awaitingAppraisalId = 0u;
_currentAppraisalId = 0u;
bool ownedBusyReference = _transactions.BusyCount > 0;
_transactions.CompleteUse(0u);
if (!ownedBusyReference)
StateChanged?.Invoke();
}
else
{
_currentAppraisalId = 0u;
StateChanged?.Invoke();
}
_sendExamine?.Invoke(0u);
}
/// <summary>
@ -791,8 +766,11 @@ public sealed class ItemInteractionController : IDisposable
if (!EnsureInventoryRequestReady())
return false;
_sendUseWithTarget?.Invoke(sourceGuid, targetGuid);
_transactions.IncrementBusyCount();
_runtimeTransactions.TryDispatchTargetedUse(
sourceGuid,
targetGuid,
_sendUseWithTarget,
incrementBusy: true);
return true;
}
@ -827,7 +805,7 @@ public sealed class ItemInteractionController : IDisposable
else
{
_sendUse!(objectId);
_transactions.IncrementBusyCount();
_runtimeTransactions.IncrementBusyCount();
}
return true;
}
@ -926,8 +904,11 @@ public sealed class ItemInteractionController : IDisposable
}
break;
case ItemPolicyActionKind.SendUseWithTarget:
_sendUseWithTarget?.Invoke(action.ObjectId, action.TargetId);
acted |= _sendUseWithTarget is not null;
acted |= _runtimeTransactions.TryDispatchTargetedUse(
action.ObjectId,
action.TargetId,
_sendUseWithTarget,
incrementBusy: false);
break;
case ItemPolicyActionKind.SetGroundObject:
_requestExternalContainer?.Invoke(action.ObjectId);
@ -944,7 +925,7 @@ public sealed class ItemInteractionController : IDisposable
}
else
{
_transactions.IncrementBusyCount();
_runtimeTransactions.IncrementBusyCount();
}
break;
case ItemPolicyActionKind.Reject:
@ -965,7 +946,7 @@ public sealed class ItemInteractionController : IDisposable
}
private ItemUseRequestReservation BeginUseRequestReservation()
=> _transactions.BeginUseRequestReservation();
=> _runtimeTransactions.BeginUseRequestReservation();
private void ExecutePlacementActions(System.Collections.Generic.IReadOnlyList<ItemPolicyAction> actions)
{
@ -1160,8 +1141,8 @@ public sealed class ItemInteractionController : IDisposable
}
/// <summary>Retail UseDone (0x01C7) releases one UI busy reference.</summary>
public void CompleteUse(uint _)
=> _transactions.CompleteUse(0u);
public void CompleteUse(uint error)
=> _runtimeTransactions.CompleteUse(error);
/// <summary>Session teardown drops every outstanding client UI busy reference.</summary>
public void ClearBusy()
@ -1176,22 +1157,18 @@ public sealed class ItemInteractionController : IDisposable
{
PendingBackpackPlacement? pendingPlacement = _pendingBackpackPlacement;
_pendingBackpackPlacement = null;
// The canonical owner publishes its reset to Runtime observers, while
// this controller preserves the pre-J4 single UI notification emitted
// by InteractionState.ResetSession below.
// Runtime owns the transaction reset. This controller preserves the
// pre-J4 single UI notification emitted by InteractionState below.
_transactions.StateChanged -= OnTransactionStateChanged;
try
{
_transactions.ResetSession();
_runtimeTransactions.ResetSession();
}
finally
{
if (!_disposed)
_transactions.StateChanged += OnTransactionStateChanged;
}
_awaitingAppraisalId = 0;
_currentAppraisalId = 0;
_lastUseMs = long.MinValue / 2;
_consumedPrimaryClickTarget = 0u;
_consumedPrimaryClickMs = long.MinValue / 2;
@ -1238,13 +1215,7 @@ public sealed class ItemInteractionController : IDisposable
}
private bool ConsumeUseThrottle()
{
long now = _nowMs();
if (now - _lastUseMs < RetailUseThrottleMs)
return false;
_lastUseMs = now;
return true;
}
=> _runtimeTransactions.TryConsumeUseThrottle(_nowMs());
private static bool IsContainer(ClientObject item)
=> item.ContainerTypeHint != 0