fix(runtime): restore interaction completion ownership
Preserve prepublication local motion completion, require the PartArray enter-world lifecycle port, and balance deferred Use busy ownership across dispatch and cancellation. Reconcile the completed GameWindow connected gates and add regression coverage. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
5c955c36ec
commit
f9736ece6c
26 changed files with 675 additions and 68 deletions
|
|
@ -37,6 +37,32 @@ public readonly record struct PendingInventoryRequest(
|
|||
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
|
||||
|
|
@ -78,6 +104,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private readonly Func<bool> _dragOnPlayerOpensSecureTrade;
|
||||
private readonly Action<string>? _systemMessage;
|
||||
private readonly AutoWieldController _autoWield;
|
||||
private readonly Action<uint, ItemUseRequestReservation>? _requestUse;
|
||||
|
||||
private long _lastUseMs = long.MinValue / 2;
|
||||
private uint _consumedPrimaryClickTarget;
|
||||
|
|
@ -86,6 +113,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private ulong _nextInventoryRequestToken;
|
||||
private PendingBackpackPlacement? _pendingBackpackPlacement;
|
||||
private PendingInventoryRequest? _pendingInventoryRequest;
|
||||
private ulong _useReservationGeneration;
|
||||
private bool _disposed;
|
||||
|
||||
public ItemInteractionController(
|
||||
|
|
@ -118,7 +146,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
Action<uint, uint, uint, uint>? sendSplitToContainer = null,
|
||||
Action<uint>? requestExternalContainer = null,
|
||||
CombatState? combatState = null,
|
||||
Action<CombatMode>? sendChangeCombatMode = null)
|
||||
Action<CombatMode>? sendChangeCombatMode = null,
|
||||
Action<uint, ItemUseRequestReservation>? requestUse = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
|
|
@ -147,6 +176,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_stackSplitQuantity = stackSplitQuantity;
|
||||
_dragOnPlayerOpensSecureTrade = dragOnPlayerOpensSecureTrade ?? (() => true);
|
||||
_systemMessage = systemMessage;
|
||||
_requestUse = requestUse;
|
||||
_interactionState = interactionState ?? new InteractionState();
|
||||
_interactionState.Changed += OnInteractionModeChanged;
|
||||
_objects.ObjectMoved += OnInventoryObjectMoved;
|
||||
|
|
@ -757,12 +787,28 @@ public sealed class ItemInteractionController : IDisposable
|
|||
/// </summary>
|
||||
public bool ExecuteConfirmedUse(uint objectId)
|
||||
{
|
||||
if (objectId == 0u || _sendUse is null)
|
||||
if (objectId == 0u || (_requestUse is null && _sendUse is null))
|
||||
return false;
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
_sendUse(objectId);
|
||||
_busyCount++;
|
||||
if (_requestUse is not null)
|
||||
{
|
||||
ItemUseRequestReservation reservation = BeginUseRequestReservation();
|
||||
try
|
||||
{
|
||||
_requestUse(objectId, reservation);
|
||||
}
|
||||
catch
|
||||
{
|
||||
reservation.CancelBeforeDispatch();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_sendUse!(objectId);
|
||||
_busyCount++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -815,6 +861,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
{
|
||||
uint openedOrUsed = 0;
|
||||
bool acted = false;
|
||||
bool busyOwnedByUseReservation = false;
|
||||
foreach (var action in actions)
|
||||
{
|
||||
switch (action.Kind)
|
||||
|
|
@ -835,9 +882,27 @@ public sealed class ItemInteractionController : IDisposable
|
|||
// coalesce the pair to one packet.
|
||||
if (openedOrUsed != action.ObjectId)
|
||||
{
|
||||
_sendUse?.Invoke(action.ObjectId);
|
||||
if (_requestUse is not null)
|
||||
{
|
||||
ItemUseRequestReservation reservation =
|
||||
BeginUseRequestReservation();
|
||||
try
|
||||
{
|
||||
_requestUse(action.ObjectId, reservation);
|
||||
}
|
||||
catch
|
||||
{
|
||||
reservation.CancelBeforeDispatch();
|
||||
throw;
|
||||
}
|
||||
busyOwnedByUseReservation = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_sendUse?.Invoke(action.ObjectId);
|
||||
}
|
||||
openedOrUsed = action.ObjectId;
|
||||
acted |= _sendUse is not null;
|
||||
acted |= _requestUse is not null || _sendUse is not null;
|
||||
}
|
||||
break;
|
||||
case ItemPolicyActionKind.SendUseWithTarget:
|
||||
|
|
@ -853,7 +918,14 @@ public sealed class ItemInteractionController : IDisposable
|
|||
acted = true;
|
||||
break;
|
||||
case ItemPolicyActionKind.IncrementBusy:
|
||||
_busyCount++;
|
||||
if (busyOwnedByUseReservation)
|
||||
{
|
||||
busyOwnedByUseReservation = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_busyCount++;
|
||||
}
|
||||
break;
|
||||
case ItemPolicyActionKind.Reject:
|
||||
if (!string.IsNullOrWhiteSpace(action.Message))
|
||||
|
|
@ -872,6 +944,21 @@ public sealed class ItemInteractionController : IDisposable
|
|||
return acted;
|
||||
}
|
||||
|
||||
private ItemUseRequestReservation BeginUseRequestReservation()
|
||||
{
|
||||
ulong generation = _useReservationGeneration;
|
||||
_busyCount++;
|
||||
StateChanged?.Invoke();
|
||||
return new ItemUseRequestReservation(dispatched =>
|
||||
{
|
||||
if (generation != _useReservationGeneration || dispatched)
|
||||
return;
|
||||
if (_busyCount > 0)
|
||||
_busyCount--;
|
||||
StateChanged?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
private void ExecutePlacementActions(System.Collections.Generic.IReadOnlyList<ItemPolicyAction> actions)
|
||||
{
|
||||
foreach (var action in actions)
|
||||
|
|
@ -1120,6 +1207,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
public void ClearBusy()
|
||||
{
|
||||
if (_busyCount == 0) return;
|
||||
_useReservationGeneration++;
|
||||
_busyCount = 0;
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
|
@ -1133,6 +1221,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
{
|
||||
PendingBackpackPlacement? pendingPlacement = _pendingBackpackPlacement;
|
||||
_pendingBackpackPlacement = null;
|
||||
_useReservationGeneration++;
|
||||
_busyCount = 0;
|
||||
_pendingInventoryRequest = null;
|
||||
_lastUseMs = long.MinValue / 2;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue