feat(ui): complete retail cursor state machine
Port every reachable ClientUISystem cursor branch through the production DAT enum map, preserve global-default versus widget-local event ordering, and surface the registered OS fallback. Route all four toolbar stance indicators through the same combat toggle command as the key binding, with golden DAT and transition conformance coverage. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
05f6222865
commit
6fcc510d5d
16 changed files with 535 additions and 164 deletions
|
|
@ -2009,7 +2009,8 @@ public sealed class GameWindow : IDisposable
|
|||
// Retail UpdateCursorState (0x00564630) keys target-mode
|
||||
// valid/invalid off the SmartBox found object — the world
|
||||
// entity under the cursor, self included.
|
||||
worldTargetProvider: () => PickWorldGuidAtCursor(includeSelf: true) ?? 0u);
|
||||
worldTargetProvider: () => PickWorldGuidAtCursor(includeSelf: true) ?? 0u,
|
||||
combatModeProvider: () => Combat.CurrentMode);
|
||||
var retailCursorManager = new RetailCursorManager(_dats!, _datLock);
|
||||
_characterSheetProvider = new AcDream.App.UI.Layout.CharacterSheetProvider(
|
||||
Objects, LocalPlayer,
|
||||
|
|
@ -2111,6 +2112,7 @@ public sealed class GameWindow : IDisposable
|
|||
(type, icon, under, over, effects) => iconComposer.GetIcon(type, icon, under, over, effects),
|
||||
UseItemByGuid,
|
||||
Combat,
|
||||
ToggleLiveCombatMode,
|
||||
_itemInteractionController,
|
||||
(index, guid) => _liveSession?.SendAddShortcut(index, guid),
|
||||
index => _liveSession?.SendRemoveShortcut(index),
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@ public sealed class RetailCursorManager
|
|||
private readonly RetailCursorResolver _globalCursors;
|
||||
private readonly Dictionary<uint, RawImage> _imagesBySurface = new();
|
||||
private readonly HashSet<uint> _missingSurfaces = new();
|
||||
private CursorFeedbackKind _lastKind = (CursorFeedbackKind)(-1);
|
||||
private UiCursorMedia _lastCursor;
|
||||
private readonly HashSet<RetailGlobalCursorKind> _reportedFallbacks = new();
|
||||
private bool _hasLayerState;
|
||||
private RetailGlobalCursorKind _lastGlobalKind;
|
||||
private UiCursorMedia _lastWidgetCursor;
|
||||
private UiCursorMedia _lastAppliedCursor;
|
||||
private StandardCursor? _lastStandardCursor;
|
||||
|
||||
public RetailCursorManager(DatCollection dats, object datLock)
|
||||
{
|
||||
|
|
@ -27,31 +31,51 @@ public sealed class RetailCursorManager
|
|||
|
||||
public void Apply(IEnumerable<IMouse> mice, CursorFeedback feedback)
|
||||
{
|
||||
if (feedback.Cursor.IsValid && TryGetImage(feedback.Cursor.File, out var image))
|
||||
foreach (RetailCursorLayer layer in PlanApplication(
|
||||
_hasLayerState,
|
||||
_lastGlobalKind,
|
||||
_lastWidgetCursor,
|
||||
feedback.GlobalKind,
|
||||
feedback.Cursor))
|
||||
{
|
||||
ApplyCustom(mice, feedback.Cursor, image);
|
||||
_lastKind = feedback.Kind;
|
||||
_lastCursor = feedback.Cursor;
|
||||
return;
|
||||
if (layer == RetailCursorLayer.Widget
|
||||
&& feedback.Cursor.IsValid
|
||||
&& TryGetImage(feedback.Cursor.File, out var image))
|
||||
{
|
||||
ApplyCustom(mice, feedback.Cursor, image);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyGlobal(mice, feedback.GlobalKind);
|
||||
}
|
||||
}
|
||||
|
||||
if (_globalCursors.TryResolve(feedback.Kind, out var globalCursor)
|
||||
_hasLayerState = true;
|
||||
_lastGlobalKind = feedback.GlobalKind;
|
||||
_lastWidgetCursor = feedback.Cursor;
|
||||
}
|
||||
|
||||
private void ApplyGlobal(IEnumerable<IMouse> mice, RetailGlobalCursorKind kind)
|
||||
{
|
||||
if (_globalCursors.TryResolve(kind, out var globalCursor)
|
||||
&& TryGetImage(globalCursor.File, out var globalImage))
|
||||
{
|
||||
ApplyCustom(mice, globalCursor, globalImage);
|
||||
_lastKind = feedback.Kind;
|
||||
_lastCursor = globalCursor;
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyStandard(mice, StandardCursorFor(feedback.Kind));
|
||||
_lastKind = feedback.Kind;
|
||||
_lastCursor = default;
|
||||
if (_reportedFallbacks.Add(kind))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[D.2b] retail cursor {kind} could not be resolved from DAT; " +
|
||||
"using the registered OS cursor adaptation (AP-72).");
|
||||
}
|
||||
ApplyStandard(mice, StandardCursorFor(kind));
|
||||
}
|
||||
|
||||
private void ApplyCustom(IEnumerable<IMouse> mice, UiCursorMedia cursorMedia, RawImage image)
|
||||
{
|
||||
if (_lastCursor.Equals(cursorMedia))
|
||||
if (_lastStandardCursor is null && _lastAppliedCursor.Equals(cursorMedia))
|
||||
return;
|
||||
|
||||
foreach (var mouse in mice)
|
||||
|
|
@ -63,12 +87,14 @@ public sealed class RetailCursorManager
|
|||
if (cursor.Type != CursorType.Custom)
|
||||
cursor.Type = CursorType.Custom;
|
||||
}
|
||||
|
||||
_lastAppliedCursor = cursorMedia;
|
||||
_lastStandardCursor = null;
|
||||
}
|
||||
|
||||
private void ApplyStandard(IEnumerable<IMouse> mice, StandardCursor desired)
|
||||
{
|
||||
if (_lastCursor.Equals(default(UiCursorMedia)) && _lastKind != (CursorFeedbackKind)(-1)
|
||||
&& StandardCursorFor(_lastKind) == desired)
|
||||
if (_lastStandardCursor == desired)
|
||||
return;
|
||||
|
||||
foreach (var mouse in mice)
|
||||
|
|
@ -85,6 +111,9 @@ public sealed class RetailCursorManager
|
|||
if (cursor.StandardCursor != standard)
|
||||
cursor.StandardCursor = standard;
|
||||
}
|
||||
|
||||
_lastAppliedCursor = default;
|
||||
_lastStandardCursor = desired;
|
||||
}
|
||||
|
||||
private bool TryGetImage(uint renderSurfaceId, out RawImage image)
|
||||
|
|
@ -122,21 +151,47 @@ public sealed class RetailCursorManager
|
|||
}
|
||||
}
|
||||
|
||||
private static StandardCursor StandardCursorFor(CursorFeedbackKind kind)
|
||||
internal static IReadOnlyList<RetailCursorLayer> PlanApplication(
|
||||
bool hasLayerState,
|
||||
RetailGlobalCursorKind previousGlobal,
|
||||
UiCursorMedia previousWidget,
|
||||
RetailGlobalCursorKind currentGlobal,
|
||||
UiCursorMedia currentWidget)
|
||||
{
|
||||
bool globalChanged = !hasLayerState || previousGlobal != currentGlobal;
|
||||
bool widgetChanged = !hasLayerState || previousWidget != currentWidget;
|
||||
if (!globalChanged && !widgetChanged)
|
||||
return Array.Empty<RetailCursorLayer>();
|
||||
|
||||
var result = new List<RetailCursorLayer>(2);
|
||||
if (globalChanged)
|
||||
result.Add(RetailCursorLayer.Global);
|
||||
|
||||
if (widgetChanged)
|
||||
{
|
||||
RetailCursorLayer widgetResult = currentWidget.IsValid
|
||||
? RetailCursorLayer.Widget
|
||||
: RetailCursorLayer.Global;
|
||||
if (result.Count == 0 || result[^1] != widgetResult)
|
||||
result.Add(widgetResult);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static StandardCursor StandardCursorFor(RetailGlobalCursorKind kind)
|
||||
=> kind switch
|
||||
{
|
||||
CursorFeedbackKind.Text => StandardCursor.IBeam,
|
||||
CursorFeedbackKind.WindowMove => StandardCursor.ResizeAll,
|
||||
CursorFeedbackKind.ResizeHorizontal => StandardCursor.HResize,
|
||||
CursorFeedbackKind.ResizeVertical => StandardCursor.VResize,
|
||||
CursorFeedbackKind.ResizeDiagonalNwse => StandardCursor.NwseResize,
|
||||
CursorFeedbackKind.ResizeDiagonalNesw => StandardCursor.NeswResize,
|
||||
CursorFeedbackKind.Drag => StandardCursor.Hand,
|
||||
CursorFeedbackKind.DragAccept => StandardCursor.ResizeAll,
|
||||
CursorFeedbackKind.DragReject => StandardCursor.NotAllowed,
|
||||
CursorFeedbackKind.TargetPending => StandardCursor.Crosshair,
|
||||
CursorFeedbackKind.TargetValid => StandardCursor.ResizeAll,
|
||||
CursorFeedbackKind.TargetInvalid => StandardCursor.NotAllowed,
|
||||
RetailGlobalCursorKind.Use or RetailGlobalCursorKind.UseFound => StandardCursor.Hand,
|
||||
RetailGlobalCursorKind.TargetPending => StandardCursor.Crosshair,
|
||||
RetailGlobalCursorKind.TargetValid => StandardCursor.ResizeAll,
|
||||
RetailGlobalCursorKind.TargetInvalid => StandardCursor.NotAllowed,
|
||||
_ => StandardCursor.Arrow,
|
||||
};
|
||||
}
|
||||
|
||||
internal enum RetailCursorLayer
|
||||
{
|
||||
Global,
|
||||
Widget,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ internal sealed class RetailCursorResolver
|
|||
_datLock = datLock;
|
||||
}
|
||||
|
||||
public bool TryResolve(CursorFeedbackKind kind, out UiCursorMedia cursor)
|
||||
public bool TryResolve(RetailGlobalCursorKind kind, out UiCursorMedia cursor)
|
||||
{
|
||||
cursor = default;
|
||||
if (!RetailCursorCatalog.TryGetGlobalCursor(kind, out var spec))
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using AcDream.Core.Combat;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -9,6 +11,10 @@ namespace AcDream.App.UI;
|
|||
public enum CursorFeedbackKind
|
||||
{
|
||||
Default,
|
||||
Combat,
|
||||
Use,
|
||||
Examine,
|
||||
Busy,
|
||||
Text,
|
||||
WindowMove,
|
||||
ResizeHorizontal,
|
||||
|
|
@ -23,7 +29,42 @@ public enum CursorFeedbackKind
|
|||
TargetInvalid,
|
||||
}
|
||||
|
||||
public readonly record struct CursorFeedback(CursorFeedbackKind Kind, UiCursorMedia Cursor = default)
|
||||
/// <summary>Retail <c>Target_Mode</c> values consumed by UpdateCursorState.</summary>
|
||||
public enum RetailCursorTargetMode
|
||||
{
|
||||
None = 0,
|
||||
Use = 1,
|
||||
Examine = 2,
|
||||
UseTarget = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The global/default cursor selected by ClientUISystem. Widget-local cursor
|
||||
/// media is a separate layer and may override this through CheckCursor.
|
||||
/// </summary>
|
||||
public enum RetailGlobalCursorKind
|
||||
{
|
||||
Default,
|
||||
DefaultFound,
|
||||
MeleeOrMissile,
|
||||
MeleeOrMissileFound,
|
||||
Magic,
|
||||
MagicFound,
|
||||
Examine,
|
||||
ExamineFound,
|
||||
Use,
|
||||
UseFound,
|
||||
Busy,
|
||||
BusyFound,
|
||||
TargetPending,
|
||||
TargetValid,
|
||||
TargetInvalid,
|
||||
}
|
||||
|
||||
public readonly record struct CursorFeedback(
|
||||
CursorFeedbackKind Kind,
|
||||
UiCursorMedia Cursor = default,
|
||||
RetailGlobalCursorKind GlobalKind = RetailGlobalCursorKind.Default)
|
||||
{
|
||||
public static CursorFeedback Default { get; } = new(CursorFeedbackKind.Default);
|
||||
}
|
||||
|
|
@ -37,19 +78,26 @@ public readonly record struct CursorFeedbackSnapshot(
|
|||
bool HoverWindowMove = false,
|
||||
bool HoverUi = false,
|
||||
bool HoverTextEdit = false,
|
||||
uint HoverTargetGuid = 0);
|
||||
uint HoverTargetGuid = 0,
|
||||
bool? HoverTargetCompatible = null,
|
||||
int BusyCount = 0,
|
||||
RetailCursorTargetMode TargetMode = RetailCursorTargetMode.None,
|
||||
CombatMode CombatMode = CombatMode.NonCombat);
|
||||
|
||||
public sealed class CursorFeedbackController
|
||||
{
|
||||
private readonly ItemInteractionController? _itemInteraction;
|
||||
private readonly Func<uint>? _worldTargetProvider;
|
||||
private readonly Func<CombatMode> _combatModeProvider;
|
||||
|
||||
public CursorFeedbackController(
|
||||
ItemInteractionController? itemInteraction = null,
|
||||
Func<uint>? worldTargetProvider = null)
|
||||
Func<uint>? worldTargetProvider = null,
|
||||
Func<CombatMode>? combatModeProvider = null)
|
||||
{
|
||||
_itemInteraction = itemInteraction;
|
||||
_worldTargetProvider = worldTargetProvider;
|
||||
_combatModeProvider = combatModeProvider ?? (() => CombatMode.NonCombat);
|
||||
}
|
||||
|
||||
public CursorFeedback Current { get; private set; } = CursorFeedback.Default;
|
||||
|
|
@ -65,13 +113,18 @@ public sealed class CursorFeedbackController
|
|||
// 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.
|
||||
uint hoverTarget = 0;
|
||||
if (_itemInteraction?.IsTargetModeActive == true)
|
||||
{
|
||||
hoverTarget = hover is not null
|
||||
RetailCursorTargetMode targetMode = _itemInteraction?.IsTargetModeActive == true
|
||||
? RetailCursorTargetMode.UseTarget
|
||||
: RetailCursorTargetMode.None;
|
||||
uint hoverTarget = hover is null
|
||||
? _worldTargetProvider?.Invoke() ?? 0u
|
||||
: targetMode == RetailCursorTargetMode.UseTarget
|
||||
? FindHoveredItemSlot(hover)?.ItemId ?? 0u
|
||||
: _worldTargetProvider?.Invoke() ?? 0u;
|
||||
}
|
||||
: 0u;
|
||||
bool? hoverTargetCompatible = targetMode == RetailCursorTargetMode.UseTarget
|
||||
&& hoverTarget != 0
|
||||
? _itemInteraction?.IsCurrentTargetCompatible(hoverTarget)
|
||||
: null;
|
||||
|
||||
var snapshot = new CursorFeedbackSnapshot(
|
||||
DragPayload: root.DragPayload,
|
||||
|
|
@ -82,10 +135,14 @@ public sealed class CursorFeedbackController
|
|||
HoverWindowMove: root.HoverWindowMove,
|
||||
HoverUi: hover is not null,
|
||||
HoverTextEdit: hover?.IsEditControl == true,
|
||||
HoverTargetGuid: hoverTarget);
|
||||
HoverTargetGuid: hoverTarget,
|
||||
HoverTargetCompatible: hoverTargetCompatible,
|
||||
BusyCount: _itemInteraction?.BusyCount ?? 0,
|
||||
TargetMode: targetMode,
|
||||
CombatMode: _combatModeProvider());
|
||||
|
||||
var kind = ResolveKind(snapshot);
|
||||
Current = new CursorFeedback(kind, ResolveCursor(root.Captured, hover, kind));
|
||||
Current = new CursorFeedback(kind, ResolveCursor(root.Captured, hover), ResolveGlobalKind(snapshot));
|
||||
return Current;
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +153,7 @@ public sealed class CursorFeedbackController
|
|||
}
|
||||
|
||||
public CursorFeedback Resolve(CursorFeedbackSnapshot snapshot)
|
||||
=> new(ResolveKind(snapshot));
|
||||
=> new(ResolveKind(snapshot), GlobalKind: ResolveGlobalKind(snapshot));
|
||||
|
||||
private CursorFeedbackKind ResolveKind(CursorFeedbackSnapshot snapshot)
|
||||
{
|
||||
|
|
@ -119,11 +176,15 @@ public sealed class CursorFeedbackController
|
|||
if (snapshot.WindowMoveActive)
|
||||
return CursorFeedbackKind.WindowMove;
|
||||
|
||||
if (_itemInteraction?.IsTargetModeActive == true)
|
||||
RetailCursorTargetMode targetMode = EffectiveTargetMode(snapshot);
|
||||
if (targetMode == RetailCursorTargetMode.UseTarget)
|
||||
{
|
||||
if (snapshot.HoverTargetGuid != 0)
|
||||
{
|
||||
return _itemInteraction.IsCurrentTargetCompatible(snapshot.HoverTargetGuid)
|
||||
bool compatible = snapshot.HoverTargetCompatible
|
||||
?? _itemInteraction?.IsCurrentTargetCompatible(snapshot.HoverTargetGuid)
|
||||
?? false;
|
||||
return compatible
|
||||
? CursorFeedbackKind.TargetValid
|
||||
: CursorFeedbackKind.TargetInvalid;
|
||||
}
|
||||
|
|
@ -136,15 +197,78 @@ public sealed class CursorFeedbackController
|
|||
return CursorFeedbackKind.TargetPending;
|
||||
}
|
||||
|
||||
if (snapshot.BusyCount > 0)
|
||||
return CursorFeedbackKind.Busy;
|
||||
|
||||
if (targetMode == RetailCursorTargetMode.Use)
|
||||
return CursorFeedbackKind.Use;
|
||||
|
||||
if (targetMode == RetailCursorTargetMode.Examine)
|
||||
return CursorFeedbackKind.Examine;
|
||||
|
||||
if (snapshot.HoverWindowMove)
|
||||
return CursorFeedbackKind.WindowMove;
|
||||
|
||||
if (snapshot.HoverTextEdit)
|
||||
return CursorFeedbackKind.Text;
|
||||
|
||||
if (snapshot.CombatMode is CombatMode.Melee or CombatMode.Missile or CombatMode.Magic)
|
||||
return CursorFeedbackKind.Combat;
|
||||
|
||||
return CursorFeedbackKind.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Faithful port of ClientUISystem::UpdateCursorState @ 0x00564630.
|
||||
/// The found-object pair is selected first; busy then target mode then
|
||||
/// combat mode replace it in exactly retail's order.
|
||||
/// </summary>
|
||||
internal RetailGlobalCursorKind ResolveGlobalKind(CursorFeedbackSnapshot snapshot)
|
||||
{
|
||||
bool found = snapshot.HoverTargetGuid != 0;
|
||||
|
||||
if (snapshot.BusyCount > 0)
|
||||
return found ? RetailGlobalCursorKind.BusyFound : RetailGlobalCursorKind.Busy;
|
||||
|
||||
switch (EffectiveTargetMode(snapshot))
|
||||
{
|
||||
case RetailCursorTargetMode.Use:
|
||||
return found ? RetailGlobalCursorKind.UseFound : RetailGlobalCursorKind.Use;
|
||||
case RetailCursorTargetMode.Examine:
|
||||
return found ? RetailGlobalCursorKind.ExamineFound : RetailGlobalCursorKind.Examine;
|
||||
case RetailCursorTargetMode.UseTarget:
|
||||
if (!found)
|
||||
return RetailGlobalCursorKind.TargetPending;
|
||||
bool compatible = snapshot.HoverTargetCompatible
|
||||
?? _itemInteraction?.IsCurrentTargetCompatible(snapshot.HoverTargetGuid)
|
||||
?? false;
|
||||
return compatible
|
||||
? RetailGlobalCursorKind.TargetValid
|
||||
: RetailGlobalCursorKind.TargetInvalid;
|
||||
case RetailCursorTargetMode.None:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(snapshot), snapshot.TargetMode, "Unknown retail target mode.");
|
||||
}
|
||||
|
||||
return snapshot.CombatMode switch
|
||||
{
|
||||
CombatMode.Melee or CombatMode.Missile => found
|
||||
? RetailGlobalCursorKind.MeleeOrMissileFound
|
||||
: RetailGlobalCursorKind.MeleeOrMissile,
|
||||
CombatMode.Magic => found
|
||||
? RetailGlobalCursorKind.MagicFound
|
||||
: RetailGlobalCursorKind.Magic,
|
||||
_ => found ? RetailGlobalCursorKind.DefaultFound : RetailGlobalCursorKind.Default,
|
||||
};
|
||||
}
|
||||
|
||||
private RetailCursorTargetMode EffectiveTargetMode(CursorFeedbackSnapshot snapshot)
|
||||
=> snapshot.TargetMode == RetailCursorTargetMode.None
|
||||
&& _itemInteraction?.IsTargetModeActive == true
|
||||
? RetailCursorTargetMode.UseTarget
|
||||
: snapshot.TargetMode;
|
||||
|
||||
private static CursorFeedbackKind KindForResize(ResizeEdges edges)
|
||||
{
|
||||
bool horizontal = (edges & (ResizeEdges.Left | ResizeEdges.Right)) != 0;
|
||||
|
|
@ -163,90 +287,22 @@ public sealed class CursorFeedbackController
|
|||
return CursorFeedbackKind.Default;
|
||||
}
|
||||
|
||||
private static UiCursorMedia ResolveCursor(UiElement? captured, UiElement? hover, CursorFeedbackKind kind)
|
||||
private static UiCursorMedia ResolveCursor(UiElement? captured, UiElement? hover)
|
||||
{
|
||||
foreach (var stateName in CursorStateNamesForKind(kind))
|
||||
{
|
||||
var semantic = FindCursorForState(hover, stateName);
|
||||
if (semantic.IsValid)
|
||||
return semantic;
|
||||
}
|
||||
|
||||
var capturedCursor = FindActiveCursor(captured);
|
||||
// UIElementManager::CheckCursor @ 0x0045ABF0: captured element first,
|
||||
// then the last-entered element, then the ClientUISystem default. It
|
||||
// does not search ancestors or synthesize a different UI state.
|
||||
var capturedCursor = captured?.ActiveCursor() ?? default;
|
||||
if (capturedCursor.IsValid)
|
||||
return capturedCursor;
|
||||
|
||||
var hoverCursor = FindActiveCursor(hover);
|
||||
var hoverCursor = hover?.ActiveCursor() ?? default;
|
||||
if (hoverCursor.IsValid)
|
||||
return hoverCursor;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> CursorStateNamesForKind(CursorFeedbackKind kind)
|
||||
=> kind switch
|
||||
{
|
||||
CursorFeedbackKind.DragAccept => new[]
|
||||
{
|
||||
"Drag_rollover_accept",
|
||||
"ItemSlot_DragOver_Accept",
|
||||
"ItemSlot_DragOver_DropIn",
|
||||
},
|
||||
CursorFeedbackKind.TargetValid => new[]
|
||||
{
|
||||
"Drag_rollover_accept",
|
||||
"ItemSlot_DragOver_Accept",
|
||||
"ItemSlot_DragOver_DropIn",
|
||||
"Csm_highlight",
|
||||
},
|
||||
CursorFeedbackKind.DragReject or CursorFeedbackKind.TargetInvalid => new[]
|
||||
{
|
||||
"Drag_rollover_reject",
|
||||
"ItemSlot_DragOver_Reject",
|
||||
"Csm_ghosted",
|
||||
},
|
||||
CursorFeedbackKind.Drag => new[]
|
||||
{
|
||||
"ItemSlot_DragOver_Normal",
|
||||
"Drag_rollover_accept",
|
||||
},
|
||||
CursorFeedbackKind.TargetPending => new[]
|
||||
{
|
||||
"DDDMode",
|
||||
"Csm_normal",
|
||||
},
|
||||
CursorFeedbackKind.WindowMove => new[]
|
||||
{
|
||||
"Csm_highlight",
|
||||
"Csm_normal",
|
||||
},
|
||||
_ => Array.Empty<string>(),
|
||||
};
|
||||
|
||||
private static UiCursorMedia FindCursorForState(UiElement? element, string stateName)
|
||||
{
|
||||
while (element is not null)
|
||||
{
|
||||
var cursor = element.CursorForState(stateName, allowFallback: false);
|
||||
if (cursor.IsValid)
|
||||
return cursor;
|
||||
element = element.Parent;
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
private static UiCursorMedia FindActiveCursor(UiElement? element)
|
||||
{
|
||||
while (element is not null)
|
||||
{
|
||||
var cursor = element.ActiveCursor();
|
||||
if (cursor.IsValid)
|
||||
return cursor;
|
||||
element = element.Parent;
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
private static UiItemSlot? FindHoveredItemSlot(UiElement? element)
|
||||
{
|
||||
while (element is not null)
|
||||
|
|
|
|||
|
|
@ -97,7 +97,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
uint[]? emptyDigits,
|
||||
ItemInteractionController? itemInteraction = null,
|
||||
Action<uint, uint>? sendAddShortcut = null,
|
||||
Action<uint>? sendRemoveShortcut = null)
|
||||
Action<uint>? sendRemoveShortcut = null,
|
||||
Action? toggleCombat = null)
|
||||
{
|
||||
_repo = repo;
|
||||
_combatState = combatState;
|
||||
|
|
@ -128,8 +129,16 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
}
|
||||
|
||||
// Cache the four mutually-exclusive combat-mode indicator elements.
|
||||
// Retail gmToolbarUI::ListenToElementMessage @ 0x004BEE90 handles
|
||||
// Click (message 1) from ANY id 0x10000192..0x10000195 by calling
|
||||
// ClientCombatSystem::ToggleCombatMode. Exactly one is visible, but all
|
||||
// four retain the same command binding as stance changes swap the art.
|
||||
for (int i = 0; i < CombatIndicatorIds.Length; i++)
|
||||
{
|
||||
_combatIndicators[i] = layout.FindElement(CombatIndicatorIds[i]);
|
||||
if (_combatIndicators[i] is UiButton button)
|
||||
button.OnClick = toggleCombat;
|
||||
}
|
||||
|
||||
_characterButton = layout.FindElement(CharacterButtonId) as UiButton;
|
||||
_inventoryButton = layout.FindElement(InventoryButtonId) as UiButton;
|
||||
|
|
@ -223,11 +232,12 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
uint[]? emptyDigits = null,
|
||||
ItemInteractionController? itemInteraction = null,
|
||||
Action<uint, uint>? sendAddShortcut = null,
|
||||
Action<uint>? sendRemoveShortcut = null)
|
||||
Action<uint>? sendRemoveShortcut = null,
|
||||
Action? toggleCombat = null)
|
||||
{
|
||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
||||
peaceDigits, warDigits, emptyDigits, itemInteraction,
|
||||
sendAddShortcut, sendRemoveShortcut);
|
||||
sendAddShortcut, sendRemoveShortcut, toggleCombat);
|
||||
c.Populate();
|
||||
return c;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,28 @@ internal static class RetailCursorCatalog
|
|||
{
|
||||
public const uint CursorEnumTable = 6;
|
||||
|
||||
// ClientUISystem::UpdateCursorState (0x00564630):
|
||||
// TARGET_MODE_USE_TARGET -> enum 0x27 when no SmartBox target is found,
|
||||
// enum 0x28 when ItemHolder::IsTargetCompatibleWithTargetingObject returns true,
|
||||
// enum 0x29 when it returns false. UIElementManager::SetCursor is called
|
||||
// with hotspot (14,14) for all three.
|
||||
public static bool TryGetGlobalCursor(CursorFeedbackKind kind, out RetailCursorSpec spec)
|
||||
// Complete reachable ClientUISystem::UpdateCursorState (0x00564630)
|
||||
// catalog. Enum values 0x07..0x09 exist in the DAT but are not selected by
|
||||
// this method, so no semantics are invented for them here.
|
||||
public static bool TryGetGlobalCursor(RetailGlobalCursorKind kind, out RetailCursorSpec spec)
|
||||
{
|
||||
spec = kind switch
|
||||
{
|
||||
CursorFeedbackKind.TargetPending => new RetailCursorSpec(0x27u, 14, 14),
|
||||
CursorFeedbackKind.TargetValid => new RetailCursorSpec(0x28u, 14, 14),
|
||||
CursorFeedbackKind.TargetInvalid => new RetailCursorSpec(0x29u, 14, 14),
|
||||
RetailGlobalCursorKind.Default => new RetailCursorSpec(0x01u, 0, 0),
|
||||
RetailGlobalCursorKind.DefaultFound => new RetailCursorSpec(0x02u, 0, 0),
|
||||
RetailGlobalCursorKind.MeleeOrMissile => new RetailCursorSpec(0x03u, 0, 0),
|
||||
RetailGlobalCursorKind.MeleeOrMissileFound => new RetailCursorSpec(0x04u, 0, 0),
|
||||
RetailGlobalCursorKind.Magic => new RetailCursorSpec(0x05u, 0, 0),
|
||||
RetailGlobalCursorKind.MagicFound => new RetailCursorSpec(0x06u, 0, 0),
|
||||
RetailGlobalCursorKind.Examine => new RetailCursorSpec(0x0Au, 0, 0),
|
||||
RetailGlobalCursorKind.ExamineFound => new RetailCursorSpec(0x0Bu, 0, 0),
|
||||
RetailGlobalCursorKind.Use => new RetailCursorSpec(0x0Cu, 14, 14),
|
||||
RetailGlobalCursorKind.UseFound => new RetailCursorSpec(0x0Du, 14, 14),
|
||||
RetailGlobalCursorKind.Busy => new RetailCursorSpec(0x0Eu, 0, 0),
|
||||
RetailGlobalCursorKind.BusyFound => new RetailCursorSpec(0x0Fu, 0, 0),
|
||||
RetailGlobalCursorKind.TargetPending => new RetailCursorSpec(0x27u, 14, 14),
|
||||
RetailGlobalCursorKind.TargetValid => new RetailCursorSpec(0x28u, 14, 14),
|
||||
RetailGlobalCursorKind.TargetInvalid => new RetailCursorSpec(0x29u, 14, 14),
|
||||
_ => default,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ public sealed record ToolbarRuntimeBindings(
|
|||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||
Action<uint> UseItem,
|
||||
CombatState Combat,
|
||||
Action ToggleCombat,
|
||||
ItemInteractionController ItemInteraction,
|
||||
Action<uint, uint>? SendAddShortcut,
|
||||
Action<uint>? SendRemoveShortcut,
|
||||
|
|
@ -358,7 +359,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
ToolbarRuntimeBindings b = _bindings.Toolbar;
|
||||
ToolbarController = Layout.ToolbarController.Bind(
|
||||
layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat,
|
||||
peace, war, empty, b.ItemInteraction, b.SendAddShortcut, b.SendRemoveShortcut);
|
||||
peace, war, empty, b.ItemInteraction, b.SendAddShortcut, b.SendRemoveShortcut,
|
||||
toggleCombat: b.ToggleCombat);
|
||||
SelectedObjectController = Layout.SelectedObjectController.Bind(
|
||||
layout,
|
||||
b.Selection,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue