using AcDream.App.UI; using AcDream.Core.Items; namespace AcDream.App.Tests.UI; public sealed class CursorFeedbackControllerTests { private const uint Player = 0x50000001u; private const uint Pack = 0x50000010u; private const uint Source = 0x50000020u; private const uint Target = 0x50000021u; // USEABLE_SOURCE_CONTAINED_TARGET_REMOTE_OR_SELF — the classic healing-kit value. private const uint HealthKitUseability = 0x00220008u; [Fact] public void DragState_winsOverResizeAndUsesAcceptRejectCursors() { var c = new CursorFeedbackController(); var accept = c.Resolve(new CursorFeedbackSnapshot( DragPayload: new object(), DragAccept: UiItemSlot.DragAcceptState.Accept, HoverResizeEdges: ResizeEdges.Right)); var reject = c.Resolve(new CursorFeedbackSnapshot( DragPayload: new object(), DragAccept: UiItemSlot.DragAcceptState.Reject, HoverResizeEdges: ResizeEdges.Right)); Assert.Equal(CursorFeedbackKind.DragAccept, accept.Kind); Assert.Equal(CursorFeedbackKind.DragReject, reject.Kind); } [Theory] [InlineData(ResizeEdges.Right, CursorFeedbackKind.ResizeHorizontal)] [InlineData(ResizeEdges.Bottom, CursorFeedbackKind.ResizeVertical)] [InlineData(ResizeEdges.Right | ResizeEdges.Bottom, CursorFeedbackKind.ResizeDiagonalNwse)] [InlineData(ResizeEdges.Left | ResizeEdges.Bottom, CursorFeedbackKind.ResizeDiagonalNesw)] public void ResizeEdges_chooseExpectedCursor(ResizeEdges edges, CursorFeedbackKind expected) { var c = new CursorFeedbackController(); var feedback = c.Resolve(new CursorFeedbackSnapshot(HoverResizeEdges: edges)); Assert.Equal(expected, feedback.Kind); } [Fact] public void MoveAndTextHover_useDedicatedCursorsWhenNoHigherPriorityState() { var c = new CursorFeedbackController(); Assert.Equal(CursorFeedbackKind.WindowMove, c.Resolve(new CursorFeedbackSnapshot(HoverWindowMove: true)).Kind); Assert.Equal(CursorFeedbackKind.Text, c.Resolve(new CursorFeedbackSnapshot(HoverTextEdit: true)).Kind); } [Fact] public void TargetMode_reportsValidInvalidAndPendingTargets() { var objects = SeedTargetObjects(); var interaction = new ItemInteractionController( objects, playerGuid: () => Player, sendUse: null, sendUseWithTarget: null, sendWield: null, sendDrop: null, nowMs: () => 1_000); var c = new CursorFeedbackController(interaction); Assert.True(interaction.ActivateItem(Source)); Assert.Equal(CursorFeedbackKind.TargetValid, c.Resolve(new CursorFeedbackSnapshot(HoverUi: true, HoverTargetGuid: Player)).Kind); Assert.Equal(CursorFeedbackKind.TargetInvalid, c.Resolve(new CursorFeedbackSnapshot(HoverUi: true, HoverTargetGuid: 0x5000BADu)).Kind); // Retail UpdateCursorState: no found object → PENDING, including over // UI chrome (the HoverUi→Invalid arm was a non-retail invention). Assert.Equal(CursorFeedbackKind.TargetPending, c.Resolve(new CursorFeedbackSnapshot(HoverUi: true)).Kind); Assert.Equal(CursorFeedbackKind.TargetPending, c.Resolve(new CursorFeedbackSnapshot()).Kind); } [Fact] public void UpdateFromRoot_slotItemDrivesTargetCursor_withStateArt() { var objects = SeedTargetObjects(); objects.Get(Source)!.TargetType = (uint)ItemType.Misc; // a tool that targets items var interaction = new ItemInteractionController( objects, playerGuid: () => Player, sendUse: null, sendUseWithTarget: null, sendWield: null, sendDrop: null, nowMs: () => 1_000); var root = new UiRoot { Width = 800, Height = 600 }; var slot = new UiItemSlot { Left = 10, Top = 10, Width = 32, Height = 32, }; var acceptCursor = new UiCursorMedia(0x06009999u, 11, 12); slot.SetStateCursors(new Dictionary { ["Drag_rollover_accept"] = acceptCursor, }); slot.SetItem(Target, iconTexture: 1u); root.AddChild(slot); root.OnMouseMove(20, 20); var c = new CursorFeedbackController(interaction); Assert.True(interaction.ActivateItem(Source)); var feedback = c.Update(root); Assert.Equal(CursorFeedbackKind.TargetValid, feedback.Kind); Assert.Equal(acceptCursor, feedback.Cursor); } [Fact] public void UpdateFromRoot_worldProviderDrivesTargetCursor_whenUiNotHovered() { var objects = SeedTargetObjects(); var interaction = new ItemInteractionController( objects, playerGuid: () => Player, sendUse: null, sendUseWithTarget: null, sendWield: null, sendDrop: null, nowMs: () => 1_000); var root = new UiRoot { Width = 800, Height = 600 }; // nothing under the mouse root.OnMouseMove(400, 300); var c = new CursorFeedbackController(interaction, worldTargetProvider: () => Player); Assert.True(interaction.ActivateItem(Source)); // World hover (retail SmartBox found object) drives valid/invalid… Assert.Equal(CursorFeedbackKind.TargetValid, c.Update(root).Kind); // …but UI occludes the world: hovering a plain panel → PENDING even // though the world provider would return a valid target. var panel = new UiPanel { Left = 390, Top = 290, Width = 40, Height = 40 }; root.AddChild(panel); Assert.Equal(CursorFeedbackKind.TargetPending, c.Update(root).Kind); } private static ClientObjectTable SeedTargetObjects() { var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = Player, Name = "Player", Type = ItemType.Creature, }); objects.AddOrUpdate(new ClientObject { ObjectId = Pack, Name = "Backpack", Type = ItemType.Container, ItemsCapacity = 24, }); objects.MoveItem(Pack, Player, 0); objects.AddOrUpdate(new ClientObject { ObjectId = Source, Name = "Health Kit", Type = ItemType.Misc, Useability = HealthKitUseability, TargetType = (uint)ItemType.Creature, // retail kind gate: kits target creatures/players }); objects.MoveItem(Source, Pack, 0); objects.AddOrUpdate(new ClientObject { ObjectId = Target, Name = "Target", Type = ItemType.Misc, }); objects.MoveItem(Target, Pack, 1); return objects; } }