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>
237 lines
8.8 KiB
C#
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);
|
|
}
|
|
}
|