fix(items): finish corpse pickup and combat switching

Port retail's first-slot ShowPendingInPlayer path for double-click loot and carry the current owned-container destination through deferred pickup. Retire the previous ground-container view as soon as a replacement is requested so its range close cannot cancel ACE's active MoveTo chain.

Preserve active-combat weapon intent across ACE's authoritative wand-to-missile stance tail while leaving peace-mode switches unchanged.

Release build succeeds and all 5,885 tests pass with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 18:35:29 +02:00
parent d51a0fc825
commit 0392c6d721
17 changed files with 631 additions and 81 deletions

View file

@ -1136,7 +1136,13 @@ public sealed class GameWindow : IDisposable
// rotating to face the target). Only set for close-range Use/PickUp;
// far-range sends fire the wire packet immediately at SendUse/SendPickUp
// time. Cleared before the deferred send fires — single-fire, no retry.
private (uint Guid, bool IsPickup)? _pendingPostArrivalAction;
private PendingPostArrivalAction? _pendingPostArrivalAction;
private readonly record struct PendingPostArrivalAction(
uint Guid,
bool IsPickup,
uint DestinationContainerId = 0u,
int Placement = 0);
// R5-V2: the local player's CPhysicsObj stand-in — owns the player's
// TargetManager voyeur system, registered in _physicsHosts so remote
@ -2127,7 +2133,13 @@ public sealed class GameWindow : IDisposable
_itemInteractionController = new AcDream.App.UI.ItemInteractionController(
Objects,
playerGuid: () => _playerServerGuid,
sendUse: g => _liveSession?.SendUse(g),
// ItemHolder::UseObject is the common policy owner, but world
// activation still has to pass through the application-level
// use adapter. It owns speculative turn/move, the close-range
// deferred send, and ACE's far-range MoveTo callback. Calling
// WorldSession directly here made keyboard R approach a corpse
// without completing the same open transaction as double-click.
sendUse: SendUse,
sendExamine: g => _liveSession?.SendAppraise(g),
sendUseWithTarget: (source, target) => _liveSession?.SendUseWithTarget(source, target),
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
@ -2144,8 +2156,19 @@ public sealed class GameWindow : IDisposable
playerOnGround: GetDebugPlayerOnGround,
inNonCombatMode: () => Combat.CurrentMode
== AcDream.Core.Combat.CombatMode.NonCombat,
combatState: Combat,
sendChangeCombatMode: mode =>
_liveSession?.SendChangeCombatMode(mode),
isComponentPack: magicCatalog.IsComponentPack,
placeInBackpack: SendPickUp,
backpackContainerId: () =>
_retailUiRuntime?.InventoryPanelController?.CurrentOpenContainerId
?? _playerServerGuid,
// Retail ItemHolder::DetermineUseResult treats children of the
// current ground object as loot. Keep this late-bound because
// ViewContents can replace/close the external container while
// the retained controller remains alive.
groundObjectId: () => _externalContainers.CurrentContainerId,
sendSplitToWorld: (item, amount) =>
_liveSession?.SendStackableSplitTo3D(item, amount),
selectedObjectId: () => _selection.SelectedObjectId ?? 0u,
@ -13258,9 +13281,6 @@ public sealed class GameWindow : IDisposable
return;
}
if (IsExternalContainerTarget(guid))
_externalContainers.RequestOpen(guid);
// B.6/R4-V5: install a speculative local TurnToObject/MoveToObject
// through the player's MoveToManager so close-range Use rotates the
// body to face before the action fires. For FAR targets, ACE's
@ -13278,7 +13298,7 @@ public sealed class GameWindow : IDisposable
{
// Defer the wire packet — OnAutoWalkArrivedSendDeferredAction
// will fire it after rotation completes.
_pendingPostArrivalAction = (guid, false);
_pendingPostArrivalAction = new PendingPostArrivalAction(guid, IsPickup: false);
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
Console.WriteLine($"[B.4b] use deferred (close-range, turn-first) guid=0x{guid:X8}");
return;
@ -13318,6 +13338,9 @@ public sealed class GameWindow : IDisposable
}
private void SendPickUp(uint itemGuid)
=> SendPickUp(itemGuid, _playerServerGuid, placement: 0);
private void SendPickUp(uint itemGuid, uint destinationContainerId, int placement)
{
if (_liveSession is null
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
@ -13362,7 +13385,11 @@ public sealed class GameWindow : IDisposable
if (closeRange)
{
_pendingPostArrivalAction = (itemGuid, true);
_pendingPostArrivalAction = new PendingPostArrivalAction(
itemGuid,
IsPickup: true,
destinationContainerId,
placement);
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
Console.WriteLine($"[B.5] pickup deferred (close-range, turn-first) item=0x{itemGuid:X8}");
return;
@ -13375,9 +13402,9 @@ public sealed class GameWindow : IDisposable
// ACE's chain alive.
var seq = _liveSession.NextGameActionSequence();
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
seq, itemGuid, _playerServerGuid, placement: 0);
seq, itemGuid, destinationContainerId, placement);
_liveSession.SendGameAction(body);
Console.WriteLine($"[B.5] pickup item=0x{itemGuid:X8} container=0x{_playerServerGuid:X8} seq={seq}");
Console.WriteLine($"[B.5] pickup item=0x{itemGuid:X8} container=0x{destinationContainerId:X8} placement={placement} seq={seq}");
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
string label = DescribeLiveEntity(itemGuid);
@ -13398,7 +13425,7 @@ public sealed class GameWindow : IDisposable
/// </summary>
private void OnAutoWalkArrivedSendDeferredAction()
{
if (_pendingPostArrivalAction is not (uint guid, bool isPickup) pending)
if (_pendingPostArrivalAction is not { } pending)
return;
_pendingPostArrivalAction = null;
@ -13407,18 +13434,21 @@ public sealed class GameWindow : IDisposable
return;
var seq = _liveSession.NextGameActionSequence();
if (isPickup)
if (pending.IsPickup)
{
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
seq, guid, _playerServerGuid, placement: 0);
seq,
pending.Guid,
pending.DestinationContainerId,
pending.Placement);
_liveSession.SendGameAction(body);
Console.WriteLine($"[B.5] pickup-deferred item=0x{guid:X8} container=0x{_playerServerGuid:X8} seq={seq}");
Console.WriteLine($"[B.5] pickup-deferred item=0x{pending.Guid:X8} container=0x{pending.DestinationContainerId:X8} placement={pending.Placement} seq={seq}");
}
else
{
var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, guid);
var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, pending.Guid);
_liveSession.SendGameAction(body);
Console.WriteLine($"[B.4b] use-deferred guid=0x{guid:X8} seq={seq}");
Console.WriteLine($"[B.4b] use-deferred guid=0x{pending.Guid:X8} seq={seq}");
}
}
@ -13497,20 +13527,6 @@ public sealed class GameWindow : IDisposable
return dx * dx + dy * dy <= useRadius * useRadius;
}
private bool IsExternalContainerTarget(uint guid)
{
if (Objects.Get(guid) is { } item
&& (item.ContainerTypeHint != 0u
|| item.Type.HasFlag(AcDream.Core.Items.ItemType.Container)
|| item.ItemsCapacity > 0))
{
return true;
}
return LastSpawns.TryGetValue(guid, out var spawn)
&& (spawn.ObjectDescriptionFlags.GetValueOrDefault() & 0x2000u) != 0u;
}
private void InstallSpeculativeTurnToTarget(uint targetGuid)
{
if (_playerController is not { } pc || pc.MoveTo is null) return;