acdream/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs
Erik d51a0fc825 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>
2026-07-17 16:53:49 +02:00

237 lines
8.8 KiB
C#

using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.App.World;
using AcDream.Core.Items;
using AcDream.Core.Selection;
namespace AcDream.App.Tests.UI.Layout;
public sealed class ExternalContainerControllerTests
{
private const uint Player = 0x50000001u;
private const uint Chest = 0x70000001u;
private const uint Item = 0x60000001u;
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();
public readonly ClientObjectTable Objects = new();
public readonly SelectionState Selection = new();
public readonly StackSplitQuantityState Split = new();
public readonly List<uint> Uses = new();
public readonly List<uint> NoLonger = new();
public readonly List<uint> Pickups = new();
public readonly List<(uint Item, uint Container, int Placement)> Puts = new();
public readonly List<(uint Item, uint Container, uint Placement, uint Amount)> Splits = new();
public readonly UiRoot Screen = new() { Width = 800f, Height = 600f };
public readonly UiItemList Top = new() { Width = 36f, Height = 36f };
public readonly UiItemList Containers = new() { Width = 680f, Height = 36f };
public readonly UiItemList Contents = new() { Width = 784f, Height = 32f };
public readonly RetailWindowHandle Window;
public readonly ItemInteractionController Interaction;
public readonly ExternalContainerLifecycleController Lifecycle;
public readonly ExternalContainerController Controller;
public bool InRange = true;
public Harness()
{
var root = new TestElement { Width = 800f, Height = 110f };
var close = new UiButton(
new ElementInfo { Id = ExternalContainerController.CloseButtonId, Type = 1 },
static _ => (0u, 0, 0));
var scrollbar = new UiScrollbar { Width = 784f, Height = 16f };
root.AddChild(Top);
root.AddChild(Containers);
root.AddChild(Contents);
root.AddChild(close);
root.AddChild(scrollbar);
var layout = new ImportedLayout(root, new Dictionary<uint, UiElement>
{
[ExternalContainerController.TopContainerId] = Top,
[ExternalContainerController.ContainerListId] = Containers,
[ExternalContainerController.ContentsListId] = Contents,
[ExternalContainerController.ContentsScrollbarId] = scrollbar,
[ExternalContainerController.CloseButtonId] = close,
});
Window = RetailWindowFrame.Mount(
Screen,
root,
static _ => (0u, 0, 0),
new RetailWindowFrame.Options
{
WindowName = WindowNames.ExternalContainer,
Chrome = RetailWindowChrome.Imported,
Visible = false,
Draggable = false,
Resizable = false,
});
Interaction = new ItemInteractionController(
Objects,
playerGuid: () => Player,
sendUse: Uses.Add,
sendUseWithTarget: null,
sendWield: null,
sendDrop: null,
groundObjectId: () => State.CurrentContainerId,
placeInBackpack: Pickups.Add,
sendPutItemInContainer: (item, container, placement) =>
Puts.Add((item, container, placement)),
sendSplitToContainer: (item, container, placement, amount) =>
Splits.Add((item, container, placement, amount)),
requestExternalContainer: id => State.RequestOpen(id));
Lifecycle = new ExternalContainerLifecycleController(State, Objects, NoLonger.Add);
Controller = ExternalContainerController.Bind(
layout,
State,
Objects,
Selection,
Interaction,
Split,
static (_, _, _, _, _) => 0u,
static (_, _, _, _, _) => 0u,
Uses.Add,
(item, container, placement) => Puts.Add((item, container, placement)),
(item, container, placement, amount) =>
Splits.Add((item, container, placement, amount)),
_ => InRange,
Window);
Screen.WindowManager.AttachController(WindowNames.ExternalContainer, Controller);
}
public void Open(uint id = Chest, params ContainerContentEntry[] contents)
{
Objects.AddOrUpdate(new ClientObject
{
ObjectId = id,
Type = ItemType.Container,
ItemsCapacity = 24,
Useability = ItemUseability.Remote,
});
State.RequestOpen(id);
Objects.ReplaceContents(id, contents);
State.ApplyViewContents(id);
}
public void Dispose()
{
Lifecycle.Dispose();
Interaction.Dispose();
Screen.WindowManager.Dispose();
}
}
[Fact]
public void AuthoritativeView_ShowsAuthoredStripAndPopulatesLists()
{
using var h = new Harness();
h.Open(Chest, new ContainerContentEntry(Item, 0u));
Assert.True(h.Window.IsVisible);
Assert.Equal(Chest, h.Top.GetItem(0)!.ItemId);
Assert.Equal(Item, h.Contents.GetItem(0)!.ItemId);
Assert.True(h.Contents.HorizontalScroll);
Assert.Same(h.Contents.Scroll,
Assert.IsType<UiScrollbar>(h.Window.ContentRoot.Children.Last()).Model);
}
[Fact]
public void DoubleClickLoot_UsesSharedItemPolicyAndPicksUpItem()
{
using var h = new Harness();
h.Open(Chest, new ContainerContentEntry(Item, 0u));
h.Contents.GetItem(0)!.DoubleClicked!();
Assert.Equal(new uint[] { Item }, h.Pickups);
}
[Fact]
public void CloseAndRangeExit_SendUseOnceWithoutNoLongerViewing()
{
using var h = new Harness();
h.Open();
h.InRange = false;
h.Controller.Tick();
h.Controller.Tick();
Assert.Equal(new uint[] { Chest }, h.Uses);
Assert.Empty(h.NoLonger);
Assert.False(h.Window.IsVisible);
h.State.ApplyClose(Chest);
Assert.Empty(h.NoLonger);
}
[Fact]
public void Replacement_SendsNoLongerViewingForPreviousRootExactlyOnce()
{
using var h = new Harness();
h.Open(Chest);
h.Open(0x70000002u);
Assert.Equal(new uint[] { Chest }, h.NoLonger);
Assert.Equal(0x70000002u, h.State.CurrentContainerId);
Assert.True(h.Window.IsVisible);
}
[Fact]
public void DropOwnedPartialStackIntoChest_UsesSplitRequestWithoutLocalMove()
{
using var h = new Harness();
h.Open();
var item = new ClientObject { ObjectId = Item, StackSize = 5, Type = ItemType.Misc };
h.Objects.AddOrUpdate(item);
h.Objects.MoveItem(Item, Player, 0);
h.Selection.Select(Item, SelectionChangeSource.Inventory);
h.Split.Reset(5u, 2u);
var source = new UiItemSlot { SourceKind = ItemDragSource.Inventory };
source.SetItem(Item, 0u);
var payload = new ItemDragPayload(Item, ItemDragSource.Inventory, 0, source);
UiItemSlot target = h.Contents.GetItem(0)!;
h.Controller.HandleDropRelease(h.Contents, target, payload);
Assert.Equal(new[] { (Item, Chest, 0u, 2u) }, h.Splits);
Assert.Equal(Player, h.Objects.Get(Item)!.ContainerId);
Assert.Empty(h.Puts);
}
}