fix(interaction): bind selection to live incarnations

Carry local WorldEntity identity through render hits, lighting pulses, and deferred movement actions so GUID reuse cannot target a replacement. Reset all session-owned selection and ItemHolder state and prevent combat auto-target during teardown.
This commit is contained in:
Erik 2026-07-21 07:07:58 +02:00
parent c271383714
commit 047a4c83b5
19 changed files with 374 additions and 42 deletions

View file

@ -1168,6 +1168,7 @@ public sealed class GameWindow : IDisposable
private readonly record struct PendingPostArrivalAction(
uint Guid,
uint LocalEntityId,
bool IsPickup,
uint DestinationContainerId = 0u,
int Placement = 0);
@ -2982,10 +2983,11 @@ public sealed class GameWindow : IDisposable
Objects.Clear();
SpellBook.Clear();
_magicRuntime?.Reset();
_itemInteractionController?.ClearBusy();
_itemInteractionController?.ResetSession();
_selection.Reset();
_outboundInteractions.Clear();
_pendingPostArrivalAction = null;
_retailSelectionScene?.Reset();
_particleVisibility.Reset();
try
{
@ -5490,14 +5492,15 @@ public sealed class GameWindow : IDisposable
() => _liveEntityPresentation?.Forget(record),
() => _entityEffects?.OnLiveEntityUnregistered(record),
() => _remoteTeleportController?.Forget(record),
() => incarnation.RunIfNoReplacement(() =>
() =>
{
if (_pendingPostArrivalAction is { Guid: var pendingGuid }
&& pendingGuid == serverGuid)
&& pendingGuid == serverGuid
&& _pendingPostArrivalAction.Value.LocalEntityId == record.LocalEntityId)
{
_pendingPostArrivalAction = null;
}
}),
},
};
if (record.WorldEntity is not { } existingEntity)
@ -13049,7 +13052,7 @@ public sealed class GameWindow : IDisposable
{
uint target = PickWorldGuidAt(x, y, includeSelf: true) ?? 0u;
if (target != 0u)
_retailSelectionScene?.BeginLightingPulse(target);
BeginSelectionLightingPulse(target);
_itemInteractionController?.PlaceIn3D(itemPayload, target);
}
}
@ -13765,16 +13768,40 @@ public sealed class GameWindow : IDisposable
private uint? PickWorldGuidAt(float mouseX, float mouseY, bool includeSelf)
{
if (_retailSelectionScene is null || _window is null)
if (_retailSelectionScene is null
|| _liveEntities is null
|| _window is null)
return null;
var camera = GetSelectionCamera();
return _retailSelectionScene.Pick(
AcDream.Core.Selection.RetailSelectionHit? hit = _retailSelectionScene.Pick(
mouseX,
mouseY,
camera.Viewport,
camera.View,
camera.Projection,
includeSelf ? 0u : _playerServerGuid);
if (hit is not { } found
|| !_liveEntities.TryGetInteractionEligibleRecord(
found.ServerGuid,
found.LocalEntityId,
out _))
{
return null;
}
return found.ServerGuid;
}
private void BeginSelectionLightingPulse(uint serverGuid)
{
if (_retailSelectionScene is null
|| _liveEntities?.TryGetRecord(serverGuid, out LiveEntityRecord record) != true
|| record.LocalEntityId is not { } localEntityId)
{
return;
}
_retailSelectionScene.BeginLightingPulse(serverGuid, localEntityId);
}
private void PickAndStoreSelection(bool useImmediately)
@ -13791,7 +13818,7 @@ public sealed class GameWindow : IDisposable
// Retail UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound
// @ 0x004E5AD0 pulses every successfully found click before it
// branches into select/use/targeted-use behavior.
_retailSelectionScene?.BeginLightingPulse(guid);
BeginSelectionLightingPulse(guid);
if (_itemInteractionController?.OfferPrimaryClick(guid)
is not null and not AcDream.App.UI.ItemPrimaryClickResult.NotActive)
@ -13890,6 +13917,13 @@ public sealed class GameWindow : IDisposable
// Close-range deferral fires the wire packet ONCE on
// MoveToComplete(None) (turn-first done), not a retry of an
// earlier failed send. No re-send path.
if (_liveEntities?.TryGetInteractionEligibleRecord(guid, out LiveEntityRecord useTarget)
!= true
|| useTarget.LocalEntityId is not { } useTargetLocalId)
{
return;
}
bool closeRange = IsCloseRangeTarget(guid);
InstallSpeculativeTurnToTarget(guid);
@ -13897,7 +13931,10 @@ public sealed class GameWindow : IDisposable
{
// Defer the wire packet — OnAutoWalkArrivedSendDeferredAction
// will fire it after rotation completes.
_pendingPostArrivalAction = new PendingPostArrivalAction(guid, IsPickup: false);
_pendingPostArrivalAction = new PendingPostArrivalAction(
guid,
useTargetLocalId,
IsPickup: false);
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
Console.WriteLine($"[B.4b] use deferred (close-range, turn-first) guid=0x{guid:X8}");
return;
@ -13979,6 +14016,14 @@ public sealed class GameWindow : IDisposable
// overlay reports arrival.
//
// 2026-05-16: simplified — FIRST send on arrival, not a retry.
if (_liveEntities?.TryGetInteractionEligibleRecord(
itemGuid,
out LiveEntityRecord pickupTarget) != true
|| pickupTarget.LocalEntityId is not { } pickupTargetLocalId)
{
return;
}
bool closeRange = IsCloseRangeTarget(itemGuid);
InstallSpeculativeTurnToTarget(itemGuid);
@ -13986,6 +14031,7 @@ public sealed class GameWindow : IDisposable
{
_pendingPostArrivalAction = new PendingPostArrivalAction(
itemGuid,
pickupTargetLocalId,
IsPickup: true,
destinationContainerId,
placement);
@ -14031,6 +14077,13 @@ public sealed class GameWindow : IDisposable
if (_liveSession is null
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
return;
if (_liveEntities?.TryGetInteractionEligibleRecord(
pending.Guid,
pending.LocalEntityId,
out _) != true)
{
return;
}
var seq = _liveSession.NextGameActionSequence();
if (pending.IsPickup)