Corrects an incomplete reading from #411's original investigation. UIElement_SmartBoxWrapper::FindObject @0x004E5430 calls SmartBox::set_found_object(itemID, 0xFFFFFFFF) whenever the hovered UI element (m_pElementLastOver) casts to UIElement_UIItem (class 0x10000032) — UNCONDITIONALLY, not gated on target mode, and returns WITHOUT running the 3D raycast. ClientUISystem:: UpdateCursorState @0x00564630 computes its "found" flag ONCE at the top of the function (ebx = SmartBox::get_found_object_id() != 0, @0x00564642) and every later branch (default/melee-missile/magic/ use/examine/use-target/busy) reads that SAME flag — so hovering an occupied item cell shows the cursor's "...Found" variant in EVERY mode, not only during an active UseTarget selection. CursorFeedbackController.Update(UiRoot) already had the item-hover special case wired from an earlier round but incorrectly gated it to TargetMode.UseTarget only; that one-line gate is removed. ResolveGlobalKind needed no changes at all — it already read the snapshot's HoverTargetGuid unconditionally across every mode. Two new tests pin the widened behavior in ordinary peace mode and in combat mode. Live-DAT-independent (pure decomp + unit fixture). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
523 lines
21 KiB
C#
523 lines
21 KiB
C#
using AcDream.App.UI;
|
|
using AcDream.Core.Combat;
|
|
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.Left, CursorFeedbackKind.ResizeHorizontal, 0x06006128u)]
|
|
[InlineData(ResizeEdges.Right, CursorFeedbackKind.ResizeHorizontal, 0x06006128u)]
|
|
[InlineData(ResizeEdges.Top, CursorFeedbackKind.ResizeVertical, 0x06005E66u)]
|
|
[InlineData(ResizeEdges.Bottom, CursorFeedbackKind.ResizeVertical, 0x06005E66u)]
|
|
[InlineData(ResizeEdges.Left | ResizeEdges.Top, CursorFeedbackKind.ResizeDiagonalNwse, 0x06006126u)]
|
|
[InlineData(ResizeEdges.Right | ResizeEdges.Bottom, CursorFeedbackKind.ResizeDiagonalNwse, 0x06006126u)]
|
|
[InlineData(ResizeEdges.Left | ResizeEdges.Bottom, CursorFeedbackKind.ResizeDiagonalNesw, 0x06006127u)]
|
|
[InlineData(ResizeEdges.Right | ResizeEdges.Top, CursorFeedbackKind.ResizeDiagonalNesw, 0x06006127u)]
|
|
public void ResizeEdges_chooseExpectedRetailCursor(
|
|
ResizeEdges edges,
|
|
CursorFeedbackKind expected,
|
|
uint expectedDid)
|
|
{
|
|
var c = new CursorFeedbackController();
|
|
|
|
var feedback = c.Resolve(new CursorFeedbackSnapshot(HoverResizeEdges: edges));
|
|
|
|
Assert.Equal(expected, feedback.Kind);
|
|
Assert.Equal(new UiCursorMedia(expectedDid, 16, 16), feedback.Cursor);
|
|
}
|
|
|
|
[Fact]
|
|
public void MoveAndTextHover_useDedicatedCursorsWhenNoHigherPriorityState()
|
|
{
|
|
var c = new CursorFeedbackController();
|
|
|
|
Assert.Equal(CursorFeedbackKind.WindowMove,
|
|
c.Resolve(new CursorFeedbackSnapshot(HoverWindowMove: true)).Kind);
|
|
Assert.Equal(new UiCursorMedia(0x06006119u, 16, 16),
|
|
c.Resolve(new CursorFeedbackSnapshot(HoverWindowMove: true)).Cursor);
|
|
Assert.Equal(CursorFeedbackKind.Text,
|
|
c.Resolve(new CursorFeedbackSnapshot(HoverTextEdit: true)).Kind);
|
|
}
|
|
|
|
[Fact]
|
|
public void ActiveSyntheticResize_keepsControlCursorOverAuthoredCapturedElement()
|
|
{
|
|
var root = new UiRoot { Width = 400, Height = 300 };
|
|
var window = new UiPanel
|
|
{
|
|
Left = 50,
|
|
Top = 40,
|
|
Width = 150,
|
|
Height = 100,
|
|
Draggable = true,
|
|
Resizable = true,
|
|
};
|
|
var content = new UiPanel { Width = 150, Height = 100 };
|
|
var authored = new UiCursorMedia(0x06009999u, 3, 4);
|
|
content.SetStateCursors(new Dictionary<string, UiCursorMedia> { [""] = authored });
|
|
window.AddChild(content);
|
|
root.AddChild(window);
|
|
var controller = new CursorFeedbackController();
|
|
|
|
root.OnMouseMove(199, 90); // inside the five-pixel right-edge resizebar region
|
|
Assert.Equal(new UiCursorMedia(0x06006128u, 16, 16), controller.Update(root).Cursor);
|
|
|
|
root.OnMouseDown(UiMouseButton.Left, 199, 90);
|
|
root.OnMouseMove(100, 90); // leave the edge while resize capture remains active
|
|
|
|
CursorFeedback active = controller.Update(root);
|
|
Assert.Equal(CursorFeedbackKind.ResizeHorizontal, active.Kind);
|
|
Assert.Equal(new UiCursorMedia(0x06006128u, 16, 16), active.Cursor);
|
|
}
|
|
|
|
[Fact]
|
|
public void CornerGrip_ShowsTheDiagonalResizeCursor_NotTheStraightOne()
|
|
{
|
|
// Campaign CH slice CH6a, user-gate round 2 item 6: hovering a directly
|
|
// authored corner Resizebar grip (not just the generic proximity edge)
|
|
// must resolve to the retail diagonal cursor, matching the same pinned
|
|
// ids as ResizeEdges_chooseExpectedRetailCursor (0x06006126 for the
|
|
// NW-SE UL/LR diagonal).
|
|
var root = new UiRoot { Width = 400, Height = 300 };
|
|
var window = new UiPanel
|
|
{
|
|
Left = 50, Top = 40, Width = 150, Height = 100,
|
|
Draggable = true, Resizable = true,
|
|
};
|
|
var grip = new UiResizeGrip
|
|
{
|
|
BorderLocation = UiResizeGrip.Border.LowerRight,
|
|
Left = 145, Top = 95, Width = 5, Height = 5,
|
|
};
|
|
window.AddChild(grip);
|
|
root.AddChild(window);
|
|
var controller = new CursorFeedbackController();
|
|
|
|
var gs = grip.ScreenPosition;
|
|
root.OnMouseMove((int)(gs.X + 2), (int)(gs.Y + 2));
|
|
|
|
CursorFeedback feedback = controller.Update(root);
|
|
Assert.Equal(CursorFeedbackKind.ResizeDiagonalNwse, feedback.Kind);
|
|
Assert.Equal(new UiCursorMedia(0x06006126u, 16, 16), feedback.Cursor);
|
|
}
|
|
|
|
[Fact]
|
|
public void EdgeGrip_ShowsTheStraightResizeCursor()
|
|
{
|
|
var root = new UiRoot { Width = 400, Height = 300 };
|
|
var window = new UiPanel
|
|
{
|
|
Left = 50, Top = 40, Width = 150, Height = 100,
|
|
Draggable = true, Resizable = true,
|
|
};
|
|
var grip = new UiResizeGrip
|
|
{
|
|
BorderLocation = UiResizeGrip.Border.Bottom,
|
|
Left = 5, Top = 95, Width = 140, Height = 5,
|
|
};
|
|
window.AddChild(grip);
|
|
root.AddChild(window);
|
|
var controller = new CursorFeedbackController();
|
|
|
|
var gs = grip.ScreenPosition;
|
|
root.OnMouseMove((int)(gs.X + 2), (int)(gs.Y + 2));
|
|
|
|
CursorFeedback feedback = controller.Update(root);
|
|
Assert.Equal(CursorFeedbackKind.ResizeVertical, feedback.Kind);
|
|
Assert.Equal(new UiCursorMedia(0x06005E66u, 16, 16), feedback.Cursor);
|
|
}
|
|
|
|
[Fact]
|
|
public void AuthoredWidgetCursor_winsUntilSyntheticWindowMoveStarts()
|
|
{
|
|
var root = new UiRoot { Width = 400, Height = 300 };
|
|
var window = new UiPanel
|
|
{
|
|
Left = 50,
|
|
Top = 40,
|
|
Width = 150,
|
|
Height = 100,
|
|
Draggable = true,
|
|
};
|
|
var dragRegion = new UiPanel { Width = 150, Height = 100 };
|
|
var authored = new UiCursorMedia(0x06008888u, 7, 8);
|
|
dragRegion.SetStateCursors(new Dictionary<string, UiCursorMedia> { [""] = authored });
|
|
window.AddChild(dragRegion);
|
|
root.AddChild(window);
|
|
var controller = new CursorFeedbackController();
|
|
|
|
root.OnMouseMove(100, 90);
|
|
Assert.Equal(authored, controller.Update(root).Cursor);
|
|
|
|
root.OnMouseDown(UiMouseButton.Left, 100, 90);
|
|
Assert.Equal(new UiCursorMedia(0x06006119u, 16, 16), controller.Update(root).Cursor);
|
|
|
|
root.OnMouseMove(300, 200);
|
|
Assert.Equal(new UiCursorMedia(0x06006119u, 16, 16), controller.Update(root).Cursor);
|
|
}
|
|
|
|
[Fact]
|
|
public void UiLock_suppressesAuthoredMoveHandleCursor()
|
|
{
|
|
// The dragbar (retail UIElement_Dragbar) carries an authored MD_Data_Cursor
|
|
// — the four-arrow move cursor. With the UI locked the handle cannot move
|
|
// its window, so it must not advertise the move affordance either (the
|
|
// radar's authored drag button hides the same way when locked).
|
|
var root = new UiRoot { Width = 800, Height = 600 };
|
|
var window = new UiPanel { Left = 100, Top = 500, Width = 610, Height = 90 };
|
|
var handle = new UiPanel
|
|
{
|
|
Left = 5, Top = 0, Width = 600, Height = 5,
|
|
WindowMoveHandle = true,
|
|
};
|
|
var authored = new UiCursorMedia(0x06007777u, 2, 2);
|
|
handle.SetStateCursors(new Dictionary<string, UiCursorMedia> { [""] = authored });
|
|
window.AddChild(handle);
|
|
root.AddChild(window);
|
|
var controller = new CursorFeedbackController();
|
|
|
|
root.OnMouseMove(110, 502);
|
|
Assert.Equal(authored, controller.Update(root).Cursor);
|
|
|
|
root.UiLocked = true;
|
|
Assert.NotEqual(authored, controller.Update(root).Cursor);
|
|
}
|
|
|
|
[Fact]
|
|
public void UiLock_suppressesSyntheticWindowControlCursors()
|
|
{
|
|
var root = new UiRoot { Width = 400, Height = 300, UiLocked = true };
|
|
root.AddChild(new UiPanel
|
|
{
|
|
Left = 50,
|
|
Top = 40,
|
|
Width = 150,
|
|
Height = 100,
|
|
Draggable = true,
|
|
Resizable = true,
|
|
});
|
|
root.OnMouseMove(199, 90);
|
|
|
|
CursorFeedback feedback = new CursorFeedbackController().Update(root);
|
|
|
|
Assert.Equal(CursorFeedbackKind.Default, feedback.Kind);
|
|
Assert.False(feedback.Cursor.IsValid);
|
|
}
|
|
|
|
[Fact]
|
|
public void TargetMode_reportsValidInvalidAndPendingTargets()
|
|
{
|
|
var objects = SeedTargetObjects();
|
|
var interaction = new ItemInteractionController(
|
|
objects,
|
|
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
|
new InteractionState(),
|
|
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,
|
|
HoverTargetCompatible: true)).Kind);
|
|
Assert.Equal(CursorFeedbackKind.TargetInvalid,
|
|
c.Resolve(new CursorFeedbackSnapshot(
|
|
HoverUi: true,
|
|
HoverTargetGuid: 0x5000BADu,
|
|
HoverTargetCompatible: false)).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_slotItemDrivesGlobalTargetCursor_withoutInventingWidgetState()
|
|
{
|
|
var objects = SeedTargetObjects();
|
|
objects.Get(Source)!.TargetType = (uint)ItemType.Misc; // a tool that targets items
|
|
var interaction = new ItemInteractionController(
|
|
objects,
|
|
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
|
new InteractionState(),
|
|
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<string, UiCursorMedia>
|
|
{
|
|
["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(RetailGlobalCursorKind.TargetValid, feedback.GlobalKind);
|
|
Assert.False(feedback.Cursor.IsValid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// #411 fix (2026-08-16): UIElement_SmartBoxWrapper::FindObject
|
|
/// @0x004E5430 calls SmartBox::set_found_object(itemID) whenever the
|
|
/// hovered UI element casts to UIElement_UIItem, UNCONDITIONALLY — not
|
|
/// only in TARGET_MODE_USE_TARGET. ClientUISystem::UpdateCursorState
|
|
/// @0x00564630 then reads that SAME found flag from every mode branch
|
|
/// (Default/Combat/Use/Examine/Busy), so hovering an occupied item cell
|
|
/// in ordinary peace mode (no active interaction mode at all) must show
|
|
/// the Found cursor variant. This is the earlier-STOPped "does retail
|
|
/// swap the pointer over inventory items" question, resolved: yes,
|
|
/// unconditionally.
|
|
/// </summary>
|
|
[Fact]
|
|
public void UpdateFromRoot_HoveringAnItemSlot_ShowsFoundCursor_InOrdinaryPeaceMode()
|
|
{
|
|
var root = new UiRoot { Width = 800, Height = 600 };
|
|
var slot = new UiItemSlot { Left = 10, Top = 10, Width = 32, Height = 32 };
|
|
slot.SetItem(Target, iconTexture: 1u);
|
|
root.AddChild(slot);
|
|
root.OnMouseMove(20, 20);
|
|
var c = new CursorFeedbackController(); // no ItemInteractionController — TargetMode.None throughout
|
|
|
|
var feedback = c.Update(root);
|
|
|
|
Assert.Equal(RetailGlobalCursorKind.DefaultFound, feedback.GlobalKind);
|
|
}
|
|
|
|
/// <summary>Same #411 fix, combat mode: the found flag is mode-
|
|
/// independent, so an item hover under an active combat stance shows
|
|
/// MeleeOrMissileFound, not the plain (not-found) MeleeOrMissile.</summary>
|
|
[Fact]
|
|
public void UpdateFromRoot_HoveringAnItemSlot_ShowsFoundCursor_InCombatMode()
|
|
{
|
|
var root = new UiRoot { Width = 800, Height = 600 };
|
|
var slot = new UiItemSlot { Left = 10, Top = 10, Width = 32, Height = 32 };
|
|
slot.SetItem(Target, iconTexture: 1u);
|
|
root.AddChild(slot);
|
|
root.OnMouseMove(20, 20);
|
|
var c = new CursorFeedbackController(combatModeProvider: () => CombatMode.Melee);
|
|
|
|
var feedback = c.Update(root);
|
|
|
|
Assert.Equal(RetailGlobalCursorKind.MeleeOrMissileFound, feedback.GlobalKind);
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdateFromRoot_worldProviderDrivesTargetCursor_whenUiNotHovered()
|
|
{
|
|
var objects = SeedTargetObjects();
|
|
var interaction = new ItemInteractionController(
|
|
objects,
|
|
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
|
new InteractionState(),
|
|
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);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenericUseAndExamineModes_driveRetailGlobalCursors()
|
|
{
|
|
var objects = SeedTargetObjects();
|
|
var interaction = new ItemInteractionController(
|
|
objects,
|
|
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
|
new InteractionState(),
|
|
playerGuid: () => Player,
|
|
sendUse: null,
|
|
sendUseWithTarget: null,
|
|
sendWield: null,
|
|
sendDrop: null);
|
|
var c = new CursorFeedbackController(interaction);
|
|
|
|
Assert.True(interaction.UseSelectedOrEnterMode(0));
|
|
Assert.Equal(RetailGlobalCursorKind.Use,
|
|
c.Resolve(new CursorFeedbackSnapshot()).GlobalKind);
|
|
Assert.Equal(RetailGlobalCursorKind.UseFound,
|
|
c.Resolve(new CursorFeedbackSnapshot(HoverTargetGuid: Target)).GlobalKind);
|
|
|
|
interaction.CancelTargetMode();
|
|
Assert.True(interaction.ExamineSelectedOrEnterMode(0));
|
|
Assert.Equal(RetailGlobalCursorKind.Examine,
|
|
c.Resolve(new CursorFeedbackSnapshot()).GlobalKind);
|
|
Assert.Equal(RetailGlobalCursorKind.ExamineFound,
|
|
c.Resolve(new CursorFeedbackSnapshot(HoverTargetGuid: Target)).GlobalKind);
|
|
}
|
|
|
|
public static TheoryData<CursorFeedbackSnapshot, RetailGlobalCursorKind> GlobalCursorCases => new()
|
|
{
|
|
{ new(), RetailGlobalCursorKind.Default },
|
|
{ new(HoverTargetGuid: Target), RetailGlobalCursorKind.DefaultFound },
|
|
{ new(CombatMode: CombatMode.Melee), RetailGlobalCursorKind.MeleeOrMissile },
|
|
{ new(HoverTargetGuid: Target, CombatMode: CombatMode.Melee), RetailGlobalCursorKind.MeleeOrMissileFound },
|
|
{ new(CombatMode: CombatMode.Missile), RetailGlobalCursorKind.MeleeOrMissile },
|
|
{ new(CombatMode: CombatMode.Magic), RetailGlobalCursorKind.Magic },
|
|
{ new(HoverTargetGuid: Target, CombatMode: CombatMode.Magic), RetailGlobalCursorKind.MagicFound },
|
|
{ new(TargetMode: RetailCursorTargetMode.Examine), RetailGlobalCursorKind.Examine },
|
|
{ new(HoverTargetGuid: Target, TargetMode: RetailCursorTargetMode.Examine), RetailGlobalCursorKind.ExamineFound },
|
|
{ new(TargetMode: RetailCursorTargetMode.Use), RetailGlobalCursorKind.Use },
|
|
{ new(HoverTargetGuid: Target, TargetMode: RetailCursorTargetMode.Use), RetailGlobalCursorKind.UseFound },
|
|
{ new(BusyCount: 1), RetailGlobalCursorKind.Busy },
|
|
{ new(HoverTargetGuid: Target, BusyCount: 1), RetailGlobalCursorKind.BusyFound },
|
|
{ new(TargetMode: RetailCursorTargetMode.UseTarget), RetailGlobalCursorKind.TargetPending },
|
|
{ new(HoverTargetGuid: Target, HoverTargetCompatible: true, TargetMode: RetailCursorTargetMode.UseTarget), RetailGlobalCursorKind.TargetValid },
|
|
{ new(HoverTargetGuid: Target, HoverTargetCompatible: false, TargetMode: RetailCursorTargetMode.UseTarget), RetailGlobalCursorKind.TargetInvalid },
|
|
};
|
|
|
|
[Theory]
|
|
[MemberData(nameof(GlobalCursorCases))]
|
|
public void GlobalCursorDecisionTree_matchesUpdateCursorState(
|
|
CursorFeedbackSnapshot snapshot,
|
|
RetailGlobalCursorKind expected)
|
|
{
|
|
var c = new CursorFeedbackController();
|
|
|
|
Assert.Equal(expected, c.Resolve(snapshot).GlobalKind);
|
|
}
|
|
|
|
[Fact]
|
|
public void BusyGlobalCursor_winsOverTargetAndCombat()
|
|
{
|
|
var c = new CursorFeedbackController();
|
|
var snapshot = new CursorFeedbackSnapshot(
|
|
HoverTargetGuid: Target,
|
|
HoverTargetCompatible: true,
|
|
BusyCount: 1,
|
|
TargetMode: RetailCursorTargetMode.UseTarget,
|
|
CombatMode: CombatMode.Magic);
|
|
|
|
Assert.Equal(RetailGlobalCursorKind.BusyFound, c.Resolve(snapshot).GlobalKind);
|
|
}
|
|
|
|
[Fact]
|
|
public void WidgetCursorPrecedence_isCapturedThenHoveredThenGlobalDefault()
|
|
{
|
|
var root = new UiRoot { Width = 200, Height = 100 };
|
|
var captured = new UiPanel { Left = 0, Top = 0, Width = 50, Height = 50 };
|
|
var hovered = new UiPanel { Left = 60, Top = 0, Width = 50, Height = 50 };
|
|
var capturedCursor = new UiCursorMedia(0x06001001u, 1, 2);
|
|
var hoveredCursor = new UiCursorMedia(0x06001002u, 3, 4);
|
|
captured.SetStateCursors(new Dictionary<string, UiCursorMedia> { [""] = capturedCursor });
|
|
hovered.SetStateCursors(new Dictionary<string, UiCursorMedia> { [""] = hoveredCursor });
|
|
root.AddChild(captured);
|
|
root.AddChild(hovered);
|
|
var c = new CursorFeedbackController();
|
|
|
|
root.OnMouseMove(10, 10);
|
|
root.OnMouseDown(UiMouseButton.Left, 10, 10);
|
|
root.OnMouseMove(70, 10);
|
|
Assert.Equal(capturedCursor, c.Update(root).Cursor);
|
|
|
|
root.OnMouseUp(UiMouseButton.Left, 70, 10);
|
|
Assert.Equal(hoveredCursor, c.Update(root).Cursor);
|
|
|
|
root.OnMouseMove(150, 75);
|
|
var fallback = c.Update(root);
|
|
Assert.False(fallback.Cursor.IsValid);
|
|
Assert.Equal(RetailGlobalCursorKind.Default, fallback.GlobalKind);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|