fix(ui): #411 — pointer swaps over inventory items unconditionally, not only in UseTarget mode

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>
This commit is contained in:
Erik 2026-08-16 23:02:42 +02:00
parent fe1bc70753
commit aed423174b
2 changed files with 68 additions and 8 deletions

View file

@ -308,6 +308,51 @@ public sealed class CursorFeedbackControllerTests
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()
{