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:
parent
fe1bc70753
commit
aed423174b
2 changed files with 68 additions and 8 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue