feat(ui): finish retail toolbar controls

Discover all seven panel launchers from their DAT panel-id attributes, route mounted panels through event-driven retained window state, and ghost unavailable panels. Port Use and Examine selection/target behavior with exact Appraise dispatch and retail cursor modes.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 12:11:53 +02:00
parent 21fefce0e0
commit d3d1c895a0
19 changed files with 478 additions and 72 deletions

View file

@ -60,6 +60,7 @@ public sealed class ItemInteractionController : IDisposable
private readonly Func<uint> _playerGuid;
private readonly Func<long> _nowMs;
private readonly Action<uint>? _sendUse;
private readonly Action<uint>? _sendExamine;
private readonly Action<uint, uint>? _sendUseWithTarget;
private readonly Action<uint, uint>? _sendWield;
private readonly Action<uint>? _sendDrop;
@ -90,6 +91,7 @@ public sealed class ItemInteractionController : IDisposable
Action<uint, uint>? sendUseWithTarget,
Action<uint, uint>? sendWield,
Action<uint>? sendDrop,
Action<uint>? sendExamine = null,
Func<long>? nowMs = null,
Action<string>? toast = null,
InteractionState? interactionState = null,
@ -108,6 +110,7 @@ public sealed class ItemInteractionController : IDisposable
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
_sendUse = sendUse;
_sendExamine = sendExamine;
_sendUseWithTarget = sendUseWithTarget;
_sendWield = sendWield;
_sendDrop = sendDrop;
@ -152,6 +155,9 @@ public sealed class ItemInteractionController : IDisposable
public bool IsTargetModeActive
=> _interactionState.Current.Kind == InteractionModeKind.UseItemOnTarget;
public bool IsAnyTargetModeActive
=> _interactionState.Current.Kind != InteractionModeKind.None;
public bool IsPendingSource(uint itemGuid)
=> itemGuid != 0 && itemGuid == PendingSourceItem;
@ -182,7 +188,8 @@ public sealed class ItemInteractionController : IDisposable
/// </summary>
public ItemPrimaryClickResult OfferPrimaryClick(uint targetGuid)
{
if (!IsTargetModeActive)
InteractionModeKind mode = _interactionState.Current.Kind;
if (mode == InteractionModeKind.None)
{
long now = _nowMs();
if (targetGuid != 0
@ -193,7 +200,25 @@ public sealed class ItemInteractionController : IDisposable
return ItemPrimaryClickResult.NotActive;
}
bool accepted = AcquireTarget(targetGuid);
bool accepted;
switch (mode)
{
case InteractionModeKind.Use:
ClearTargetMode();
accepted = ActivateItem(targetGuid);
break;
case InteractionModeKind.Examine:
ClearTargetMode();
accepted = targetGuid != 0 && _sendExamine is not null;
if (accepted)
_sendExamine!(targetGuid);
break;
case InteractionModeKind.UseItemOnTarget:
accepted = AcquireTarget(targetGuid);
break;
default:
throw new InvalidOperationException($"Unknown interaction mode {mode}.");
}
_consumedPrimaryClickTarget = targetGuid;
_consumedPrimaryClickMs = _nowMs();
return accepted
@ -204,6 +229,33 @@ public sealed class ItemInteractionController : IDisposable
public ItemPrimaryClickResult OfferSelfPrimaryClick()
=> OfferPrimaryClick(_playerGuid());
/// <summary>
/// Retail toolbar Use button: use the current selection immediately, or arm
/// one-shot <c>TARGET_MODE_USE</c> when no object is selected.
/// Retail reference: <c>gmToolbarUI::ListenToElementMessage @ 0x004BEE90</c>.
/// </summary>
public bool UseSelectedOrEnterMode(uint selectedObjectId)
{
if (selectedObjectId != 0)
return ActivateItem(selectedObjectId);
return _interactionState.EnterUse();
}
/// <summary>
/// Retail toolbar Examine button: appraise the current selection immediately,
/// or arm one-shot <c>TARGET_MODE_EXAMINE</c> when no object is selected.
/// Retail reference: <c>gmToolbarUI::ListenToElementMessage @ 0x004BEE90</c>.
/// </summary>
public bool ExamineSelectedOrEnterMode(uint selectedObjectId)
{
if (selectedObjectId == 0)
return _interactionState.EnterExamine();
if (_sendExamine is null)
return false;
_sendExamine(selectedObjectId);
return true;
}
public bool ActivateItem(uint itemGuid)
{
if (itemGuid == 0) return false;
@ -264,7 +316,7 @@ public sealed class ItemInteractionController : IDisposable
public void CancelTargetMode()
{
if (!IsTargetModeActive) return;
if (!IsAnyTargetModeActive) return;
ClearTargetMode();
}