Visual gate on the item-interaction slice found two target-use bugs: the valid-target bullseye showed over inventory ITEMS (a coat is not a valid healing-kit target) while the paperdoll doll showed blocked (it should mean SELF). - TargetCompatible is now a faithful port of ItemHolder::IsTargetCompatibleWithTargetingObject (0x00588070): a location constraint applies only when the least-limited target-use bit (ItemUses::GetLeastLimitedTargetUse 0x004fcd50, new ItemUseability.LeastLimitedTargetUse) is Contained/Wielded; the player as target requires the Self bit (IsUseable_SelfTarget 0x004fcd30); and EVERY target passes retail's kind gate source._targetType & target->InqType(). The previous hand-rolled arms (Remote => accept anything, invented Viewed/Contained accepts, self path skipping the kind gate) were the yellow-over-items bug. Retail's tradeState refusal is skipped (no trade state yet). - The paperdoll doll viewport now carries UseTargetGuidProvider = player and Clicked -> AcquireSelfTarget (UiViewport gains an opt-in Clicked handler), so hovering the doll resolves the self cursor and clicking it applies the armed kit to yourself. - AcquireTarget logs one [use-target] line (wire useability, TargetType mask, target kind, decision) so live refusals are diagnosable from the launch log. - Test fixtures updated from the guessed kit useability 0x000A0008 to the real USEABLE_SOURCE_CONTAINED_TARGET_REMOTE_OR_SELF (0x00220008, ACE Usable.SourceContainedTargetRemoteOrSelf) + TargetType masks; new coverage pins the kind gate, the Self-bit requirement, the missing-mask-matches-nothing shape, and the Contained location rule. Full suite green (3,294). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
157 lines
5.5 KiB
C#
157 lines
5.5 KiB
C#
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);
|
|
Assert.Equal(CursorFeedbackKind.TargetInvalid,
|
|
c.Resolve(new CursorFeedbackSnapshot(HoverUi: true)).Kind);
|
|
Assert.Equal(CursorFeedbackKind.TargetPending,
|
|
c.Resolve(new CursorFeedbackSnapshot()).Kind);
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdateFromRoot_usesUseTargetGuidProviderBeforeSlotItem()
|
|
{
|
|
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 };
|
|
var slot = new UiItemSlot
|
|
{
|
|
Left = 10,
|
|
Top = 10,
|
|
Width = 32,
|
|
Height = 32,
|
|
UseTargetGuidProvider = () => Player,
|
|
};
|
|
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(acceptCursor, feedback.Cursor);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|