fix(items): complete retail corpse looting feedback

Route corpse Use through the shared ItemHolder policy so Stuck corpses open instead of being sent as pickup requests. Restore the framed, horizontally resizable external-container strip and use a compact initial width. Port retail's target-list pending item projection so loot is marked in the chosen inventory slot without changing canonical ownership before the server confirms.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 16:53:49 +02:00
parent 20ce67b625
commit d51a0fc825
13 changed files with 275 additions and 102 deletions

View file

@ -134,6 +134,30 @@ public sealed class ItemInteractionControllerTests
Assert.Equal(chest, h.GroundObject);
}
[Fact]
public void CorpseUse_UsesStuckContainerInsteadOfTryingToPickItUp()
{
var h = new Harness();
const uint corpse = 0x70000002u;
h.Objects.AddOrUpdate(new ClientObject
{
ObjectId = corpse,
Type = ItemType.Container,
ItemsCapacity = 24,
Useability = ItemUseability.Remote,
PublicWeenieBitfield = (uint)(
PublicWeenieFlags.Openable
| PublicWeenieFlags.Stuck
| PublicWeenieFlags.Corpse),
});
Assert.True(h.Controller.UseSelectedOrEnterMode(corpse));
Assert.Equal(new uint[] { corpse }, h.Uses);
Assert.Equal(new uint[] { corpse }, h.ExternalRequests);
Assert.Empty(h.Puts);
}
[Fact]
public void DropOwnedItemOnOpenExternalContainer_SendsPutWithoutOptimisticMove()
{

View file

@ -14,6 +14,40 @@ public sealed class ExternalContainerControllerTests
private sealed class TestElement : UiElement { }
[Fact]
public void WindowPolicy_AddsOuterFrameAndAllowsHorizontalResize()
{
var screen = new UiRoot { Width = 1280f, Height = 720f };
var root = new TestElement
{
Left = 0f,
Top = 400f,
Width = 800f,
Height = 110f,
};
RetailWindowFrame.Options options =
ExternalContainerController.CreateWindowOptions(root);
RetailWindowHandle handle = RetailWindowFrame.Mount(
screen,
root,
static _ => (0u, 0, 0),
options);
var frame = Assert.IsType<UiNineSlicePanel>(handle.OuterFrame);
Assert.Equal(710f, frame.Width);
Assert.Equal(120f, frame.Height);
Assert.Equal(170f, frame.MinWidth);
Assert.True(frame.Draggable);
Assert.True(frame.Resizable);
Assert.True(frame.ResizeX);
Assert.False(frame.ResizeY);
Assert.Equal(ResizeEdges.Left | ResizeEdges.Right, frame.ResizableEdges);
Assert.True(frame.ConstrainResizeToParent);
Assert.False(frame.DrawCenterFill);
Assert.Equal(700f, root.Width);
}
private sealed class Harness : IDisposable
{
public readonly ExternalContainerState State = new();

View file

@ -627,6 +627,68 @@ public class InventoryControllerTests
Assert.Equal(Player, objects.Get(0xFFFFu)!.ContainerId); // moved locally (instant)
}
[Fact]
public void LootDrop_InsertsWaitingProjectionAtChosenSlotUntilServerConfirms()
{
const uint chest = 0x70000001u;
const uint loot = 0x70000002u;
var (layout, grid, _, _, _, _, _, _) = BuildLayout();
var objects = new ClientObjectTable();
SeedContained(objects, 0xAu, Player, slot: 0);
SeedContained(objects, 0xBu, Player, slot: 1);
SeedContained(objects, loot, chest, slot: 0, type: ItemType.Misc);
var selection = new SelectionState();
selection.Select(loot, SelectionChangeSource.ExternalContainer);
var puts = new List<(uint item, uint container, int placement)>();
using var ctrl = Bind(layout, objects, puts: puts, selection: selection);
var source = new UiItemSlot { SourceKind = ItemDragSource.Ground };
source.SetItem(loot, 0u);
var payload = new ItemDragPayload(loot, ItemDragSource.Ground, 0, source);
ctrl.HandleDropRelease(grid, grid.GetItem(1)!, payload);
Assert.Equal(new[] { (loot, Player, 1) }, puts);
Assert.Equal(chest, objects.Get(loot)!.ContainerId);
Assert.Equal(loot, grid.GetItem(1)!.ItemId);
Assert.True(grid.GetItem(1)!.WaitingVisual);
Assert.True(grid.GetItem(1)!.Selected);
Assert.Equal(0xBu, grid.GetItem(2)!.ItemId);
objects.ApplyConfirmedServerMove(loot, Player, 0u, newSlot: 1);
UiItemSlot confirmed = Enumerable.Range(0, grid.GetNumUIItems())
.Select(i => grid.GetItem(i)!)
.Single(cell => cell.ItemId == loot);
Assert.False(confirmed.WaitingVisual);
Assert.Equal(Player, objects.Get(loot)!.ContainerId);
}
[Fact]
public void LootDrop_ServerFailureRemovesOnlyThePendingProjection()
{
const uint chest = 0x70000001u;
const uint loot = 0x70000002u;
var (layout, grid, _, _, _, _, _, _) = BuildLayout();
var objects = new ClientObjectTable();
SeedContained(objects, 0xAu, Player, slot: 0);
SeedContained(objects, loot, chest, slot: 0, type: ItemType.Misc);
var puts = new List<(uint item, uint container, int placement)>();
using var ctrl = Bind(layout, objects, puts: puts);
var source = new UiItemSlot { SourceKind = ItemDragSource.Ground };
source.SetItem(loot, 0u);
var payload = new ItemDragPayload(loot, ItemDragSource.Ground, 0, source);
ctrl.HandleDropRelease(grid, grid.GetItem(1)!, payload);
Assert.Equal(loot, grid.GetItem(1)!.ItemId);
objects.RejectMove(loot, weenieError: 0x29u);
Assert.DoesNotContain(
Enumerable.Range(0, grid.GetNumUIItems()).Select(i => grid.GetItem(i)!.ItemId),
id => id == loot);
Assert.Equal(chest, objects.Get(loot)!.ContainerId);
}
[Fact]
public void Drop_onEmptyGridCell_appendsToFirstEmpty()
{