fix(interaction): close selection lifecycle review gaps
Bind queued actions and pending inventory requests to exact live incarnations, separate optimistic placement from authoritative responses, and serialize retail-style inventory ownership across UI surfaces. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
d2bb5af453
commit
5acc3f01cf
23 changed files with 2635 additions and 168 deletions
|
|
@ -1,6 +1,7 @@
|
|||
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;
|
||||
|
|
@ -24,12 +25,18 @@ internal sealed class SelectionInteractionController
|
|||
private readonly Action<string>? _toast;
|
||||
private PendingPostArrivalAction? _pendingPostArrival;
|
||||
|
||||
private readonly record struct QueuedInteractionIdentity(
|
||||
uint ServerGuid,
|
||||
uint? LocalEntityId,
|
||||
ClientObject? ClientObject);
|
||||
|
||||
private readonly record struct PendingPostArrivalAction(
|
||||
uint ServerGuid,
|
||||
uint LocalEntityId,
|
||||
bool IsPickup,
|
||||
uint DestinationContainerId,
|
||||
int Placement);
|
||||
int Placement,
|
||||
ulong PendingPlacementToken);
|
||||
|
||||
public SelectionInteractionController(
|
||||
SelectionState selection,
|
||||
|
|
@ -69,8 +76,14 @@ internal sealed class SelectionInteractionController
|
|||
case InputAction.SelectionPickUp:
|
||||
if (_selection.SelectedObjectId is uint pickupTarget)
|
||||
{
|
||||
_outbound.Enqueue(() =>
|
||||
_items.PlaceWorldItemInBackpack(pickupTarget));
|
||||
EnqueueIdentityBound(
|
||||
pickupTarget,
|
||||
requireLiveEntity: true,
|
||||
guid =>
|
||||
{
|
||||
if (ValidatePickupTarget(guid, showToast: true))
|
||||
_items.PlaceWorldItemInBackpack(guid);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -85,12 +98,18 @@ internal sealed class SelectionInteractionController
|
|||
}
|
||||
}
|
||||
|
||||
public uint? PickAt(float mouseX, float mouseY, bool includeSelf)
|
||||
=> _query.PickAt(mouseX, mouseY, includeSelf);
|
||||
|
||||
public uint? PickAtCursor(bool includeSelf)
|
||||
=> _query.PickAtCursor(includeSelf);
|
||||
|
||||
public void PlaceDraggedItem(ItemDragPayload payload, float mouseX, float mouseY)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(payload);
|
||||
uint target = _query.PickAt(mouseX, mouseY, includeSelf: true) ?? 0u;
|
||||
if (target != 0u)
|
||||
_query.BeginLightingPulse(target);
|
||||
_items.PlaceIn3D(payload, target);
|
||||
}
|
||||
|
||||
public uint? GetSelectedOrClosestCombatTarget(bool autoTarget)
|
||||
{
|
||||
if (_selection.SelectedObjectId is { } selected
|
||||
|
|
@ -147,7 +166,13 @@ internal sealed class SelectionInteractionController
|
|||
Console.WriteLine($"[B.4b] pick guid=0x{guid:X8} name={label}");
|
||||
_toast?.Invoke($"Selected: {label}");
|
||||
if (useImmediately)
|
||||
_outbound.Enqueue(() => _items.ActivateItem(guid));
|
||||
EnqueueIdentityBound(
|
||||
guid,
|
||||
requireLiveEntity: true,
|
||||
selected =>
|
||||
{
|
||||
_items.ActivateItem(selected);
|
||||
});
|
||||
}
|
||||
|
||||
public void UseCurrentSelection()
|
||||
|
|
@ -157,11 +182,18 @@ internal sealed class SelectionInteractionController
|
|||
_toast?.Invoke("Nothing selected");
|
||||
return;
|
||||
}
|
||||
_outbound.Enqueue(() => _items.UseSelectedOrEnterMode(selected));
|
||||
EnqueueIdentityBound(
|
||||
selected,
|
||||
requireLiveEntity: false,
|
||||
guid =>
|
||||
{
|
||||
_items.UseSelectedOrEnterMode(guid);
|
||||
});
|
||||
}
|
||||
|
||||
public void SendUse(uint serverGuid)
|
||||
{
|
||||
CancelPendingApproach();
|
||||
if (!_transport.IsInWorld)
|
||||
{
|
||||
_toast?.Invoke("Not in world");
|
||||
|
|
@ -173,63 +205,104 @@ internal sealed class SelectionInteractionController
|
|||
return;
|
||||
}
|
||||
|
||||
_movement.BeginApproach(approach);
|
||||
if (approach.IsCloseRange)
|
||||
{
|
||||
_pendingPostArrival = new PendingPostArrivalAction(
|
||||
var pending = new PendingPostArrivalAction(
|
||||
serverGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
IsPickup: false,
|
||||
DestinationContainerId: 0u,
|
||||
Placement: 0);
|
||||
Placement: 0,
|
||||
PendingPlacementToken: 0u);
|
||||
bool started = _movement.BeginApproach(
|
||||
approach,
|
||||
() => _pendingPostArrival = pending);
|
||||
if (!started && _pendingPostArrival == pending)
|
||||
CancelPendingApproach();
|
||||
return;
|
||||
}
|
||||
|
||||
_movement.BeginApproach(approach);
|
||||
if (_transport.TrySendUse(serverGuid, out uint sequence))
|
||||
Console.WriteLine($"[B.4b] use guid=0x{serverGuid:X8} seq={sequence}");
|
||||
}
|
||||
|
||||
public void SendPickup(uint itemGuid, uint destinationContainerId, int placement)
|
||||
{
|
||||
CancelPendingApproach();
|
||||
if (!_transport.IsInWorld)
|
||||
{
|
||||
_toast?.Invoke("Not in world");
|
||||
CancelPickupPresentation(itemGuid);
|
||||
return;
|
||||
}
|
||||
if (_query.IsCreature(itemGuid))
|
||||
if (!ValidatePickupTarget(itemGuid, showToast: true)
|
||||
|| !_query.TryGetApproach(itemGuid, out InteractionApproach approach))
|
||||
{
|
||||
CancelPickupPresentation(itemGuid);
|
||||
return;
|
||||
}
|
||||
|
||||
ulong pendingPlacementToken = _items.TryGetPendingBackpackPlacement(
|
||||
itemGuid,
|
||||
out PendingBackpackPlacement pendingPlacement)
|
||||
? pendingPlacement.Token
|
||||
: 0u;
|
||||
if (!IsCurrentPickupPresentation(
|
||||
itemGuid,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken))
|
||||
{
|
||||
_toast?.Invoke(RetailMessages.CannotPickUpCreatures);
|
||||
return;
|
||||
}
|
||||
if (!_query.IsPickupable(itemGuid))
|
||||
{
|
||||
_toast?.Invoke(RetailMessages.CantBePickedUp(_query.Describe(itemGuid)));
|
||||
return;
|
||||
}
|
||||
if (!_query.TryGetApproach(itemGuid, out InteractionApproach approach))
|
||||
return;
|
||||
|
||||
_movement.BeginApproach(approach);
|
||||
if (approach.IsCloseRange)
|
||||
{
|
||||
_pendingPostArrival = new PendingPostArrivalAction(
|
||||
var pending = new PendingPostArrivalAction(
|
||||
itemGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
IsPickup: true,
|
||||
destinationContainerId,
|
||||
placement);
|
||||
placement,
|
||||
pendingPlacementToken);
|
||||
bool started = _movement.BeginApproach(
|
||||
approach,
|
||||
() => _pendingPostArrival = pending);
|
||||
if (!started && _pendingPostArrival == pending)
|
||||
CancelPendingApproach();
|
||||
else if (!started)
|
||||
CancelPickupPresentation(itemGuid, pendingPlacementToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_transport.TrySendPickup(
|
||||
_movement.BeginApproach(approach);
|
||||
if (!IsCurrentPickupPresentation(
|
||||
itemGuid,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
out uint sequence))
|
||||
pendingPlacementToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
uint sequence = 0u;
|
||||
if (_items.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Pickup,
|
||||
itemGuid,
|
||||
() => _transport.TrySendPickup(
|
||||
itemGuid,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
out sequence),
|
||||
pendingPlacementToken))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[B.5] pickup item=0x{itemGuid:X8} container=0x{destinationContainerId:X8} placement={placement} seq={sequence}");
|
||||
}
|
||||
else
|
||||
{
|
||||
CancelPickupPresentation(itemGuid, pendingPlacementToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fires only after natural MoveToComplete(None), never cancellation.</summary>
|
||||
|
|
@ -239,15 +312,38 @@ internal sealed class SelectionInteractionController
|
|||
return;
|
||||
_pendingPostArrival = null;
|
||||
if (!_query.IsCurrent(pending.ServerGuid, pending.LocalEntityId))
|
||||
{
|
||||
if (pending.IsPickup)
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pending.IsPickup)
|
||||
{
|
||||
_transport.TrySendPickup(
|
||||
pending.ServerGuid,
|
||||
pending.DestinationContainerId,
|
||||
pending.Placement,
|
||||
out _);
|
||||
if (!IsCurrentPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.DestinationContainerId,
|
||||
pending.Placement,
|
||||
pending.PendingPlacementToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!_items.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Pickup,
|
||||
pending.ServerGuid,
|
||||
() => _transport.TrySendPickup(
|
||||
pending.ServerGuid,
|
||||
pending.DestinationContainerId,
|
||||
pending.Placement,
|
||||
out _),
|
||||
pending.PendingPlacementToken))
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -257,10 +353,12 @@ internal sealed class SelectionInteractionController
|
|||
|
||||
public void DrainOutbound() => _outbound.Drain();
|
||||
|
||||
public void OnMoveToCancelled(WeenieError _) => CancelPendingApproach();
|
||||
|
||||
public void OnEntityHidden(uint serverGuid)
|
||||
{
|
||||
if (_pendingPostArrival?.ServerGuid == serverGuid)
|
||||
_pendingPostArrival = null;
|
||||
CancelPendingApproach();
|
||||
if (_selection.SelectedObjectId == serverGuid)
|
||||
{
|
||||
_selection.Clear(
|
||||
|
|
@ -276,7 +374,7 @@ internal sealed class SelectionInteractionController
|
|||
&& pending.ServerGuid == record.ServerGuid
|
||||
&& pending.LocalEntityId == record.LocalEntityId)
|
||||
{
|
||||
_pendingPostArrival = null;
|
||||
CancelPendingApproach();
|
||||
}
|
||||
if (!replacementExists && _selection.SelectedObjectId == record.ServerGuid)
|
||||
{
|
||||
|
|
@ -288,9 +386,85 @@ internal sealed class SelectionInteractionController
|
|||
|
||||
public void ResetSession()
|
||||
{
|
||||
CancelPendingApproach();
|
||||
_items.ResetSession();
|
||||
_selection.Reset();
|
||||
_outbound.Clear();
|
||||
_pendingPostArrival = null;
|
||||
}
|
||||
|
||||
private bool ValidatePickupTarget(uint serverGuid, bool showToast)
|
||||
{
|
||||
if (_query.IsCreature(serverGuid))
|
||||
{
|
||||
if (showToast)
|
||||
_toast?.Invoke(RetailMessages.CannotPickUpCreatures);
|
||||
return false;
|
||||
}
|
||||
if (_query.IsPickupable(serverGuid))
|
||||
return true;
|
||||
if (showToast)
|
||||
_toast?.Invoke(RetailMessages.CantBePickedUp(_query.Describe(serverGuid)));
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool EnqueueIdentityBound(
|
||||
uint serverGuid,
|
||||
bool requireLiveEntity,
|
||||
Action<uint> action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
uint? localEntityId = _query.TryCaptureIdentity(serverGuid, out uint localId)
|
||||
? localId
|
||||
: null;
|
||||
ClientObject? item = _items.TryCaptureObjectIdentity(serverGuid, out ClientObject captured)
|
||||
? captured
|
||||
: null;
|
||||
if ((requireLiveEntity && localEntityId is null)
|
||||
|| (localEntityId is null && item is null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var identity = new QueuedInteractionIdentity(serverGuid, localEntityId, item);
|
||||
_outbound.Enqueue(() =>
|
||||
{
|
||||
if (IsCurrent(identity))
|
||||
action(identity.ServerGuid);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsCurrent(QueuedInteractionIdentity identity)
|
||||
=> (identity.LocalEntityId is not uint localId
|
||||
|| _query.IsCurrent(identity.ServerGuid, localId))
|
||||
&& (identity.ClientObject is not { } item
|
||||
|| _items.IsCurrentObjectIdentity(identity.ServerGuid, item));
|
||||
|
||||
private void CancelPendingApproach()
|
||||
{
|
||||
if (_pendingPostArrival is not { } pending)
|
||||
return;
|
||||
_pendingPostArrival = null;
|
||||
if (pending.IsPickup)
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
}
|
||||
|
||||
private void CancelPickupPresentation(uint itemGuid, ulong token = 0u)
|
||||
=> _items.CancelPendingBackpackPlacement(itemGuid, token);
|
||||
|
||||
private bool IsCurrentPickupPresentation(
|
||||
uint itemGuid,
|
||||
uint destinationContainerId,
|
||||
int placement,
|
||||
ulong token)
|
||||
=> token != 0u
|
||||
&& _items.TryGetPendingBackpackPlacement(
|
||||
itemGuid,
|
||||
out PendingBackpackPlacement pending)
|
||||
&& pending.Token == token
|
||||
&& pending.ContainerId == destinationContainerId
|
||||
&& pending.Placement == placement;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue