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

@ -109,17 +109,32 @@ public sealed class CursorFeedbackController
UiElement? hover = root.Pick(root.MouseX, root.MouseY);
// Retail UpdateCursorState (0x00564630) keys the target-mode cursor off
// the SmartBox found object — the WORLD entity under the cursor. A UI
// window occludes the world (no found object → pending). The one
// UI-side source retail-style cells contribute is an occupied item
// slot's own item.
// Retail UpdateCursorState (0x00564630) keys EVERY mode's cursor off
// the SAME SmartBox found-object flag, computed once at the top of
// the function (ebx = SmartBox::get_found_object_id() != 0,
// @0x00564642) and read verbatim by every later branch — target mode
// and combat mode only pick WHICH cursor variant (Default vs.
// DefaultFound, Use vs. UseFound, ...) to show for that SAME found
// state, never whether it is set.
//
// #411 correction (2026-08-16): the found object itself is not
// world-only. UIElement_SmartBoxWrapper::FindObject @0x004E5430 runs
// every frame regardless of input focus (Global_Loop @0x004E5620)
// and, when the currently-hovered UI element (m_pElementLastOver)
// casts to UIElement_UIItem (0x10000032), calls
// SmartBox::set_found_object(itemID) directly — UNCONDITIONALLY, not
// gated on target mode — and returns WITHOUT running the 3D raycast.
// So hovering an occupied item cell sets the SAME found flag in
// EVERY mode (peace, melee/missile, magic, busy, examine, use,
// use-target), which is why retail's cursor visibly changes there —
// the earlier reading here (item slots only contribute in
// UseTarget) covered only the Valid/Invalid sub-branch, not the
// found flag driving the Default/Combat/Use/Examine/Busy Found
// variants too.
RetailCursorTargetMode targetMode = ModeFromInteraction(_itemInteraction);
uint hoverTarget = hover is null
? _worldTargetProvider?.Invoke() ?? 0u
: targetMode == RetailCursorTargetMode.UseTarget
? FindHoveredItemSlot(hover)?.ItemId ?? 0u
: 0u;
: FindHoveredItemSlot(hover)?.ItemId ?? 0u;
bool? hoverTargetCompatible = targetMode == RetailCursorTargetMode.UseTarget
&& hoverTarget != 0
? _itemInteraction?.IsCurrentTargetCompatible(hoverTarget)