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:
parent
be73bccf5a
commit
f5f7b4177f
31 changed files with 1365 additions and 432 deletions
|
|
@ -284,7 +284,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
DeferredSelectionUiAuthority selection = late.Selection;
|
||||
return new ItemInteractionController(
|
||||
d.Inventory.Objects,
|
||||
d.Inventory.Transactions,
|
||||
d.Actions.Transactions,
|
||||
d.Actions.Interaction,
|
||||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||
sendUse: null,
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
namespace AcDream.App.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Orders input-originated item interactions after the local player's movement
|
||||
/// edge has been serialized for the same frame, but before inbound network
|
||||
/// dispatch.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Silk input callbacks arrive before the retail object phase. A movement-key
|
||||
/// release and a later Use press can therefore be observed in one frame even
|
||||
/// though the movement controller has not yet emitted the release packet.
|
||||
/// Draining here preserves that event order on the wire: the release is sent
|
||||
/// first, then the one-shot interaction. ACE otherwise interprets the later
|
||||
/// MoveToState as cancellation of the Use-created MoveTo chain.
|
||||
/// </remarks>
|
||||
public sealed class OutboundInteractionQueue
|
||||
{
|
||||
private readonly Queue<Action> _pending = new();
|
||||
private uint _clearEpoch;
|
||||
|
||||
public int Count => _pending.Count;
|
||||
|
||||
public void Enqueue(Action action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
_pending.Enqueue(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drain only the actions present at the frame boundary. An action that
|
||||
/// enqueues another action leaves it for the next frame instead of making
|
||||
/// the current dispatch recursively unbounded.
|
||||
/// </summary>
|
||||
public void Drain()
|
||||
{
|
||||
int count = _pending.Count;
|
||||
uint epoch = _clearEpoch;
|
||||
while (count-- > 0 && epoch == _clearEpoch && _pending.Count > 0)
|
||||
_pending.Dequeue().Invoke();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_pending.Clear();
|
||||
_clearEpoch++;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Ui;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Interaction;
|
||||
|
|
@ -19,31 +19,17 @@ internal sealed class SelectionInteractionController
|
|||
private readonly SelectionState _selection;
|
||||
private readonly IWorldSelectionQuery _query;
|
||||
private readonly ItemInteractionController _items;
|
||||
private readonly ISelectionInteractionTransport _transport;
|
||||
private readonly RuntimeInteractionTransactionState _transactions;
|
||||
private readonly IRuntimeInteractionTransport _transport;
|
||||
private readonly IPlayerInteractionMovementSink _movement;
|
||||
private readonly OutboundInteractionQueue _outbound = new();
|
||||
private readonly PlayerApproachCompletionState _approachCompletions;
|
||||
private readonly Action<string>? _toast;
|
||||
private PendingPostArrivalPickup? _pendingPostArrival;
|
||||
|
||||
private readonly record struct QueuedInteractionIdentity(
|
||||
uint ServerGuid,
|
||||
uint? LocalEntityId,
|
||||
ClientObject? ClientObject);
|
||||
|
||||
private readonly record struct PendingPostArrivalPickup(
|
||||
uint ServerGuid,
|
||||
uint LocalEntityId,
|
||||
uint DestinationContainerId,
|
||||
int Placement,
|
||||
ulong PendingPlacementToken,
|
||||
PlayerApproachToken ApproachToken);
|
||||
|
||||
public SelectionInteractionController(
|
||||
SelectionState selection,
|
||||
IWorldSelectionQuery query,
|
||||
ItemInteractionController items,
|
||||
ISelectionInteractionTransport transport,
|
||||
IRuntimeInteractionTransport transport,
|
||||
IPlayerInteractionMovementSink movement,
|
||||
Action<string>? toast = null,
|
||||
PlayerApproachCompletionState? approachCompletions = null)
|
||||
|
|
@ -51,6 +37,7 @@ internal sealed class SelectionInteractionController
|
|||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
_query = query ?? throw new ArgumentNullException(nameof(query));
|
||||
_items = items ?? throw new ArgumentNullException(nameof(items));
|
||||
_transactions = _items.RuntimeTransactions;
|
||||
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
|
||||
_movement = movement ?? throw new ArgumentNullException(nameof(movement));
|
||||
_toast = toast;
|
||||
|
|
@ -88,13 +75,9 @@ internal sealed class SelectionInteractionController
|
|||
if (_selection.SelectedObjectId is uint pickupTarget)
|
||||
{
|
||||
EnqueueIdentityBound(
|
||||
RuntimeQueuedInteractionKind.Pickup,
|
||||
pickupTarget,
|
||||
requireLiveEntity: true,
|
||||
guid =>
|
||||
{
|
||||
if (ValidatePickupTarget(guid, showToast: true))
|
||||
_items.PlaceWorldItemInBackpack(guid);
|
||||
});
|
||||
requireLiveEntity: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -178,12 +161,9 @@ internal sealed class SelectionInteractionController
|
|||
_toast?.Invoke($"Selected: {label}");
|
||||
if (useImmediately)
|
||||
EnqueueIdentityBound(
|
||||
RuntimeQueuedInteractionKind.Activate,
|
||||
guid,
|
||||
requireLiveEntity: true,
|
||||
selected =>
|
||||
{
|
||||
_items.ActivateItem(selected);
|
||||
});
|
||||
requireLiveEntity: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -213,12 +193,9 @@ internal sealed class SelectionInteractionController
|
|||
return;
|
||||
}
|
||||
EnqueueIdentityBound(
|
||||
RuntimeQueuedInteractionKind.Use,
|
||||
selected,
|
||||
requireLiveEntity: false,
|
||||
guid =>
|
||||
{
|
||||
_items.UseSelectedOrEnterMode(guid);
|
||||
});
|
||||
requireLiveEntity: false);
|
||||
}
|
||||
|
||||
public void SendUse(uint serverGuid)
|
||||
|
|
@ -229,45 +206,19 @@ internal sealed class SelectionInteractionController
|
|||
ItemUseRequestReservation? reservation)
|
||||
{
|
||||
CancelPendingApproach();
|
||||
if (!_transport.IsInWorld)
|
||||
{
|
||||
bool ownedByPlayer = _items.IsOwnedByPlayer(serverGuid);
|
||||
RuntimeInteractionDispatchResult result =
|
||||
_transactions.TryDispatchUse(
|
||||
serverGuid,
|
||||
ownedByPlayer,
|
||||
ownedByPlayer || _query.IsUseable(serverGuid),
|
||||
reservation,
|
||||
_transport,
|
||||
out uint sequence);
|
||||
if (result == RuntimeInteractionDispatchResult.NotInWorld)
|
||||
_toast?.Invoke("Not in world");
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return;
|
||||
}
|
||||
|
||||
// ItemHolder::UseObject @ 0x00588A80 sends Event_UseEvent before
|
||||
// CPlayerSystem::UsingItem for every accepted ordinary Use. The latter
|
||||
// is presentation/local inventory policy; it never delays the wire
|
||||
// request behind a client-side TurnTo/MoveTo completion. ACE owns the
|
||||
// authoritative approach chain for spatial targets.
|
||||
if (_items.IsOwnedByPlayer(serverGuid))
|
||||
{
|
||||
SendUseNow(serverGuid, reservation);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_query.IsUseable(serverGuid))
|
||||
{
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return;
|
||||
}
|
||||
|
||||
SendUseNow(serverGuid, reservation);
|
||||
}
|
||||
|
||||
private void SendUseNow(
|
||||
uint serverGuid,
|
||||
ItemUseRequestReservation? reservation)
|
||||
{
|
||||
if (_transport.TrySendUse(serverGuid, out uint sequence))
|
||||
{
|
||||
reservation?.MarkDispatched();
|
||||
if (result == RuntimeInteractionDispatchResult.Dispatched)
|
||||
Console.WriteLine($"[B.4b] use guid=0x{serverGuid:X8} seq={sequence}");
|
||||
return;
|
||||
}
|
||||
|
||||
reservation?.CancelBeforeDispatch();
|
||||
}
|
||||
|
||||
public void SendPickup(uint itemGuid, uint destinationContainerId, int placement)
|
||||
|
|
@ -302,20 +253,38 @@ internal sealed class SelectionInteractionController
|
|||
|
||||
if (approach.IsCloseRange)
|
||||
{
|
||||
var pending = new PendingPostArrivalPickup(
|
||||
itemGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken,
|
||||
ApproachToken: default);
|
||||
bool armed = false;
|
||||
bool started = _movement.BeginApproach(
|
||||
approach,
|
||||
token => _pendingPostArrival = pending with { ApproachToken = token });
|
||||
if (!started && _pendingPostArrival?.ServerGuid == pending.ServerGuid)
|
||||
CancelPendingApproach();
|
||||
else if (!started)
|
||||
CancelPickupPresentation(itemGuid, pendingPlacementToken);
|
||||
token =>
|
||||
{
|
||||
armed = _transactions.TryArmPostArrivalPickup(
|
||||
itemGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken,
|
||||
new RuntimeInteractionApproachToken(
|
||||
token.ControllerLifetime,
|
||||
token.ApproachGeneration),
|
||||
out _);
|
||||
});
|
||||
if (!started || !armed)
|
||||
{
|
||||
if (_transactions.TryCancelPendingPickup(
|
||||
itemGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
out RuntimePendingPickup cancelled))
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
cancelled.ServerGuid,
|
||||
cancelled.PendingPlacementToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
CancelPickupPresentation(itemGuid, pendingPlacementToken);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -328,16 +297,18 @@ internal sealed class SelectionInteractionController
|
|||
{
|
||||
return;
|
||||
}
|
||||
uint sequence = 0u;
|
||||
if (_items.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Pickup,
|
||||
itemGuid,
|
||||
() => _transport.TrySendPickup(
|
||||
itemGuid,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
out sequence),
|
||||
pendingPlacementToken))
|
||||
var immediate = new RuntimePendingPickup(
|
||||
Token: 0u,
|
||||
itemGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken,
|
||||
ApproachToken: default);
|
||||
if (_transactions.TryDispatchPickup(
|
||||
immediate,
|
||||
_transport,
|
||||
out uint sequence))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[B.5] pickup item=0x{itemGuid:X8} container=0x{destinationContainerId:X8} placement={placement} seq={sequence}");
|
||||
|
|
@ -350,13 +321,28 @@ internal sealed class SelectionInteractionController
|
|||
|
||||
/// <summary>Fires only after natural MoveToComplete(None), never cancellation.</summary>
|
||||
public void OnNaturalMoveToComplete()
|
||||
=> HandleNaturalMoveToComplete();
|
||||
|
||||
private void HandleNaturalMoveToComplete()
|
||||
{
|
||||
if (_pendingPostArrival is not { } pending)
|
||||
if (_transactions.TryGetPendingPickup(out RuntimePendingPickup pending))
|
||||
HandleApproachCompletion(pending.ApproachToken, natural: true);
|
||||
}
|
||||
|
||||
private void HandleApproachCompletion(
|
||||
RuntimeInteractionApproachToken approachToken,
|
||||
bool natural)
|
||||
{
|
||||
bool accepted = _transactions.TryResolveApproachCompletion(
|
||||
approachToken,
|
||||
natural,
|
||||
out RuntimePendingPickup pending);
|
||||
if (pending.Token == 0u)
|
||||
return;
|
||||
_pendingPostArrival = null;
|
||||
if (!accepted)
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
return;
|
||||
}
|
||||
if (!_query.IsCurrent(pending.ServerGuid, pending.LocalEntityId))
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
|
|
@ -373,15 +359,10 @@ internal sealed class SelectionInteractionController
|
|||
{
|
||||
return;
|
||||
}
|
||||
if (!_items.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Pickup,
|
||||
pending.ServerGuid,
|
||||
() => _transport.TrySendPickup(
|
||||
pending.ServerGuid,
|
||||
pending.DestinationContainerId,
|
||||
pending.Placement,
|
||||
out _),
|
||||
pending.PendingPlacementToken))
|
||||
if (!_transactions.TryDispatchPickup(
|
||||
pending,
|
||||
_transport,
|
||||
out _))
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
|
|
@ -393,22 +374,29 @@ internal sealed class SelectionInteractionController
|
|||
{
|
||||
while (_approachCompletions.TryTake(out PlayerApproachCompletion completion))
|
||||
{
|
||||
if (_pendingPostArrival?.ApproachToken != completion.Token)
|
||||
continue;
|
||||
if (completion.IsNatural)
|
||||
HandleNaturalMoveToComplete();
|
||||
else
|
||||
CancelPendingApproach();
|
||||
HandleApproachCompletion(
|
||||
new RuntimeInteractionApproachToken(
|
||||
completion.Token.ControllerLifetime,
|
||||
completion.Token.ApproachGeneration),
|
||||
completion.IsNatural);
|
||||
}
|
||||
_outbound.Drain();
|
||||
_transactions.DrainOutbound(DispatchQueuedInteraction);
|
||||
}
|
||||
|
||||
public void OnMoveToCancelled(WeenieError _) => CancelPendingApproach();
|
||||
|
||||
public void OnEntityHidden(uint serverGuid)
|
||||
{
|
||||
if (_pendingPostArrival?.ServerGuid == serverGuid)
|
||||
CancelPendingApproach();
|
||||
_transactions.CancelQueuedInteractions(serverGuid);
|
||||
if (_transactions.TryCancelPendingPickup(
|
||||
serverGuid,
|
||||
localEntityId: null,
|
||||
out RuntimePendingPickup cancelled))
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
cancelled.ServerGuid,
|
||||
cancelled.PendingPlacementToken);
|
||||
}
|
||||
if (_selection.SelectedObjectId == serverGuid)
|
||||
{
|
||||
_selection.Clear(
|
||||
|
|
@ -420,11 +408,17 @@ internal sealed class SelectionInteractionController
|
|||
public void OnEntityRemoved(LiveEntityRecord record, bool replacementExists)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (_pendingPostArrival is { } pending
|
||||
&& pending.ServerGuid == record.ServerGuid
|
||||
&& pending.LocalEntityId == record.LocalEntityId)
|
||||
_transactions.CancelQueuedInteractions(
|
||||
record.ServerGuid,
|
||||
record.LocalEntityId);
|
||||
if (_transactions.TryCancelPendingPickup(
|
||||
record.ServerGuid,
|
||||
record.LocalEntityId,
|
||||
out RuntimePendingPickup cancelled))
|
||||
{
|
||||
CancelPendingApproach();
|
||||
CancelPickupPresentation(
|
||||
cancelled.ServerGuid,
|
||||
cancelled.PendingPlacementToken);
|
||||
}
|
||||
if (!replacementExists && _selection.SelectedObjectId == record.ServerGuid)
|
||||
{
|
||||
|
|
@ -443,11 +437,8 @@ internal sealed class SelectionInteractionController
|
|||
catch (Exception error) { failures.Add(error); }
|
||||
try { _selection.Reset(); }
|
||||
catch (Exception error) { failures.Add(error); }
|
||||
try { _outbound.Clear(); }
|
||||
catch (Exception error) { failures.Add(error); }
|
||||
try { _approachCompletions.Clear(); }
|
||||
catch (Exception error) { failures.Add(error); }
|
||||
_pendingPostArrival = null;
|
||||
|
||||
if (failures.Count != 0)
|
||||
throw new AggregateException(
|
||||
|
|
@ -471,11 +462,10 @@ internal sealed class SelectionInteractionController
|
|||
}
|
||||
|
||||
private bool EnqueueIdentityBound(
|
||||
RuntimeQueuedInteractionKind kind,
|
||||
uint serverGuid,
|
||||
bool requireLiveEntity,
|
||||
Action<uint> action)
|
||||
bool requireLiveEntity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
uint? localEntityId = _query.TryCaptureIdentity(serverGuid, out uint localId)
|
||||
? localId
|
||||
: null;
|
||||
|
|
@ -488,16 +478,15 @@ internal sealed class SelectionInteractionController
|
|||
return false;
|
||||
}
|
||||
|
||||
var identity = new QueuedInteractionIdentity(serverGuid, localEntityId, item);
|
||||
_outbound.Enqueue(() =>
|
||||
{
|
||||
if (IsCurrent(identity))
|
||||
action(identity.ServerGuid);
|
||||
});
|
||||
var identity = new RuntimeInteractionIdentity(
|
||||
serverGuid,
|
||||
localEntityId,
|
||||
item);
|
||||
_transactions.Enqueue(new RuntimeQueuedInteraction(kind, identity));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsCurrent(QueuedInteractionIdentity identity)
|
||||
private bool IsCurrent(RuntimeInteractionIdentity identity)
|
||||
=> (identity.LocalEntityId is not uint localId
|
||||
|| _query.IsCurrent(identity.ServerGuid, localId))
|
||||
&& (identity.ClientObject is not { } item
|
||||
|
|
@ -505,14 +494,39 @@ internal sealed class SelectionInteractionController
|
|||
|
||||
private void CancelPendingApproach()
|
||||
{
|
||||
if (_pendingPostArrival is not { } pending)
|
||||
if (!_transactions.TryCancelPendingPickup(
|
||||
out RuntimePendingPickup pending))
|
||||
return;
|
||||
_pendingPostArrival = null;
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
}
|
||||
|
||||
private void DispatchQueuedInteraction(
|
||||
RuntimeQueuedInteraction interaction)
|
||||
{
|
||||
RuntimeInteractionIdentity identity = interaction.Identity;
|
||||
if (!IsCurrent(identity))
|
||||
return;
|
||||
|
||||
switch (interaction.Kind)
|
||||
{
|
||||
case RuntimeQueuedInteractionKind.Activate:
|
||||
_items.ActivateItem(identity.ServerGuid);
|
||||
break;
|
||||
case RuntimeQueuedInteractionKind.Use:
|
||||
_items.UseSelectedOrEnterMode(identity.ServerGuid);
|
||||
break;
|
||||
case RuntimeQueuedInteractionKind.Pickup:
|
||||
if (ValidatePickupTarget(identity.ServerGuid, showToast: true))
|
||||
_items.PlaceWorldItemInBackpack(identity.ServerGuid);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unknown queued interaction kind {interaction.Kind}.");
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelPickupPresentation(uint itemGuid, ulong token = 0u)
|
||||
=> _items.CancelPendingBackpackPlacement(itemGuid, token);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,12 @@
|
|||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.App.Interaction;
|
||||
|
||||
internal interface ISelectionInteractionTransport
|
||||
{
|
||||
bool IsInWorld { get; }
|
||||
bool TrySendUse(uint serverGuid, out uint sequence);
|
||||
bool TrySendPickup(
|
||||
uint itemGuid,
|
||||
uint destinationContainerId,
|
||||
int placement,
|
||||
out uint sequence);
|
||||
}
|
||||
|
||||
internal sealed class WorldSessionSelectionInteractionTransport(
|
||||
Func<WorldSession?> session)
|
||||
: ISelectionInteractionTransport
|
||||
: IRuntimeInteractionTransport
|
||||
{
|
||||
private readonly Func<WorldSession?> _session = session
|
||||
?? throw new ArgumentNullException(nameof(session));
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
OnUseDone: error =>
|
||||
{
|
||||
_domain.Inventory.ExternalContainers.ApplyUseDone(error);
|
||||
_domain.Inventory.Transactions.CompleteUse(error);
|
||||
_domain.Actions.Transactions.CompleteUse(error);
|
||||
},
|
||||
_domain.Inventory.ItemMana,
|
||||
ExternalContainers: _domain.Inventory.ExternalContainers,
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ public sealed class GameWindow :
|
|||
// J4/J5.1: Runtime owns communication, selection, combat, and target-mode
|
||||
// state. App, UI, plugins, and live-session routing borrow exact children.
|
||||
private readonly RuntimeCommunicationState _runtimeCommunication = new();
|
||||
private readonly RuntimeActionState _runtimeActions = new();
|
||||
private readonly RuntimeActionState _runtimeActions;
|
||||
public AcDream.Core.Selection.SelectionState Selection =>
|
||||
_runtimeActions.Selection;
|
||||
public AcDream.Core.Chat.ChatLog Chat => _runtimeCommunication.Chat;
|
||||
|
|
@ -561,6 +561,7 @@ public sealed class GameWindow :
|
|||
{
|
||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||
_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);
|
||||
_runtimeActions = new RuntimeActionState(_runtimeInventory.Transactions);
|
||||
_runtimeCharacter = new RuntimeCharacterState();
|
||||
var alphaScratchBudgets =
|
||||
AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
|
||||
|
|
|
|||
|
|
@ -396,12 +396,12 @@ internal static class GameWindowShutdownManifest
|
|||
Hard(
|
||||
"runtime character state",
|
||||
live.Character.Dispose),
|
||||
Hard(
|
||||
"runtime inventory state",
|
||||
live.Inventory.Dispose),
|
||||
Hard(
|
||||
"runtime action state",
|
||||
live.Actions.Dispose),
|
||||
Hard(
|
||||
"runtime inventory state",
|
||||
live.Inventory.Dispose),
|
||||
Hard(
|
||||
"runtime entity/object lifetime",
|
||||
live.EntityObjects.Dispose),
|
||||
|
|
|
|||
|
|
@ -141,7 +141,8 @@ public static class FixtureProvider
|
|||
var selection = new AcDream.Core.Selection.SelectionState();
|
||||
var itemInteraction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(
|
||||
new InventoryTransactionState(objects)),
|
||||
new AcDream.Runtime.Gameplay.InteractionState(),
|
||||
() => SampleData.PlayerGuid,
|
||||
sendUse: null,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue