feat(ui): centralize retail selection state

This commit is contained in:
Erik 2026-07-11 00:51:20 +02:00
parent c7607f019c
commit 7983309d23
30 changed files with 591 additions and 108 deletions

View file

@ -0,0 +1,54 @@
namespace AcDream.App.UI;
public enum InteractionModeKind
{
None,
Use,
Examine,
UseItemOnTarget,
}
public readonly record struct InteractionMode(
InteractionModeKind Kind,
uint SourceObjectId = 0)
{
public static InteractionMode None => new(InteractionModeKind.None);
}
public readonly record struct InteractionModeTransition(
InteractionMode Previous,
InteractionMode Current);
/// <summary>
/// Single App-layer owner for retail's pointer interaction mode. Core selection
/// remains session truth; this state represents temporary UI orchestration.
/// </summary>
public sealed class InteractionState
{
public InteractionMode Current { get; private set; } = InteractionMode.None;
public event Action<InteractionModeTransition>? Changed;
public bool EnterUse() => Set(new InteractionMode(InteractionModeKind.Use));
public bool EnterExamine() => Set(new InteractionMode(InteractionModeKind.Examine));
public bool EnterUseItemOnTarget(uint sourceObjectId)
{
if (sourceObjectId == 0)
throw new ArgumentOutOfRangeException(nameof(sourceObjectId));
return Set(new InteractionMode(InteractionModeKind.UseItemOnTarget, sourceObjectId));
}
public bool Clear() => Set(InteractionMode.None);
private bool Set(InteractionMode mode)
{
if (Current == mode)
return false;
InteractionMode previous = Current;
Current = mode;
Changed?.Invoke(new InteractionModeTransition(previous, mode));
return true;
}
}

View file

@ -8,7 +8,7 @@ namespace AcDream.App.UI;
/// target acquisition, and drag-out drops here instead of duplicating
/// ItemHolder::UseObject fragments in each panel.
/// </summary>
public sealed class ItemInteractionController
public sealed class ItemInteractionController : IDisposable
{
private static readonly EquipMask[] AutoEquipOrder =
{
@ -55,8 +55,10 @@ public sealed class ItemInteractionController
private readonly Action<uint, uint>? _sendWield;
private readonly Action<uint>? _sendDrop;
private readonly Action<string>? _toast;
private readonly InteractionState _interactionState;
private long _lastUseMs = long.MinValue / 2;
private bool _disposed;
public ItemInteractionController(
ClientObjectTable objects,
@ -66,7 +68,8 @@ public sealed class ItemInteractionController
Action<uint, uint>? sendWield,
Action<uint>? sendDrop,
Func<long>? nowMs = null,
Action<string>? toast = null)
Action<string>? toast = null,
InteractionState? interactionState = null)
{
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
@ -76,15 +79,23 @@ public sealed class ItemInteractionController
_sendDrop = sendDrop;
_nowMs = nowMs ?? (() => Environment.TickCount64);
_toast = toast;
_interactionState = interactionState ?? new InteractionState();
_interactionState.Changed += OnInteractionModeChanged;
}
public event Action? StateChanged;
public uint PlayerGuid => _playerGuid();
public uint PendingSourceItem { get; private set; }
public InteractionState InteractionState => _interactionState;
public bool IsTargetModeActive => PendingSourceItem != 0;
public uint PendingSourceItem
=> _interactionState.Current is { Kind: InteractionModeKind.UseItemOnTarget } mode
? mode.SourceObjectId
: 0u;
public bool IsTargetModeActive
=> _interactionState.Current.Kind == InteractionModeKind.UseItemOnTarget;
public bool IsPendingSource(uint itemGuid)
=> itemGuid != 0 && itemGuid == PendingSourceItem;
@ -188,8 +199,7 @@ public sealed class ItemInteractionController
private void EnterTargetMode(uint sourceGuid)
{
PendingSourceItem = sourceGuid;
StateChanged?.Invoke();
_interactionState.EnterUseItemOnTarget(sourceGuid);
var name = _objects.Get(sourceGuid)?.Name;
if (!string.IsNullOrWhiteSpace(name))
_toast?.Invoke($"Choose a target for the {name}");
@ -197,8 +207,17 @@ public sealed class ItemInteractionController
private void ClearTargetMode()
{
PendingSourceItem = 0;
StateChanged?.Invoke();
_interactionState.Clear();
}
private void OnInteractionModeChanged(InteractionModeTransition _)
=> StateChanged?.Invoke();
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_interactionState.Changed -= OnInteractionModeChanged;
}
private bool ConsumeUseThrottle()

View file

@ -2,6 +2,7 @@ using System;
using System.Numerics;
using AcDream.App.UI;
using AcDream.Core.Items;
using AcDream.Core.Selection;
namespace AcDream.App.UI.Layout;
@ -53,7 +54,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
private static readonly Vector4 CaptionColor = new(1f, 1f, 1f, 1f);
private uint _openContainer; // 0 = the main pack (the player); else the open side bag's guid
private uint _selectedItem; // 0 = none; the panel-wide selected item (green square)
private readonly SelectionState _selection;
private readonly Action<uint>? _sendUse;
private readonly Action<uint>? _sendNoLongerViewing;
private readonly Action<uint, uint, int>? _sendPutItemInContainer; // (item, container, placement)
@ -70,6 +71,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
Func<uint> playerGuid,
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
Func<int?> strength,
SelectionState selection,
Func<string>? ownerName,
UiDatFont? datFont,
uint contentsEmptySprite,
@ -90,6 +92,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
_sendNoLongerViewing = sendNoLongerViewing;
_sendPutItemInContainer = sendPutItemInContainer;
_itemInteraction = itemInteraction;
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
WindowChromeController.BindCloseButton(layout, onClose);
@ -152,8 +155,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
// Rebuild on any change to the player's possessions.
_objects.ObjectAdded += OnObjectChanged;
_objects.ObjectMoved += OnObjectMoved;
_objects.ObjectRemoved += OnObjectChanged;
_objects.ObjectRemoved += OnObjectRemoved;
_objects.ObjectUpdated += OnObjectChanged;
_selection.Changed += OnSelectionChanged;
if (_itemInteraction is not null)
_itemInteraction.StateChanged += OnInteractionStateChanged;
@ -184,6 +188,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
Func<uint> playerGuid,
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
Func<int?> strength,
SelectionState selection,
UiDatFont? datFont,
Func<string>? ownerName = null,
uint contentsEmptySprite = 0u,
@ -194,14 +199,27 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
Action<uint, uint, int>? sendPutItemInContainer = null,
ItemInteractionController? itemInteraction = null,
Action? onClose = null)
=> new InventoryController(layout, objects, playerGuid, iconIds, strength, ownerName, datFont,
=> new InventoryController(layout, objects, playerGuid, iconIds, strength, selection,
ownerName, datFont,
contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite,
sendUse, sendNoLongerViewing, sendPutItemInContainer, itemInteraction, onClose);
sendUse, sendNoLongerViewing, sendPutItemInContainer, itemInteraction,
onClose);
private void OnObjectChanged(ClientObject o) { if (Concerns(o)) Populate(); }
private void OnObjectRemoved(ClientObject o)
{
if (_selection.SelectedObjectId == o.ObjectId)
{
_selection.Clear(
SelectionChangeSource.System,
SelectionChangeReason.SelectedObjectRemoved);
}
if (Concerns(o)) Populate();
}
private void OnObjectMoved(ClientObject o, uint from, uint to)
{ if (Concerns(o) || from == _playerGuid() || to == _playerGuid()) Populate(); }
private void OnInteractionStateChanged() => ApplyIndicators();
private void OnSelectionChanged(SelectionTransition _) => ApplyIndicators();
/// <summary>True if the object is in (or wielded by) the player — i.e. a rebuild is warranted.</summary>
private bool Concerns(ClientObject o)
@ -402,8 +420,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
private void SelectItem(uint guid)
{
if (guid == 0) return;
_selectedItem = guid;
ApplyIndicators();
_selection.Select(guid, SelectionChangeSource.Inventory);
}
/// <summary>Open a container (side bag or main pack): it becomes both the selected item and the
@ -413,7 +430,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
private void OpenContainer(uint guid)
{
if (guid == 0) return;
_selectedItem = guid; // the container cell is also the selected item
_selection.Select(guid, SelectionChangeSource.Inventory);
uint open = EffectiveOpen();
if (guid == open) { ApplyIndicators(); return; } // already open — just move the square
@ -444,7 +461,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
var cell = list.GetItem(i);
if (cell is null) continue;
bool pendingTargetSource = _itemInteraction?.IsPendingSource(cell.ItemId) == true;
cell.Selected = cell.ItemId != 0 && cell.ItemId == _selectedItem && !pendingTargetSource;
cell.Selected = cell.ItemId != 0
&& cell.ItemId == _selection.SelectedObjectId
&& !pendingTargetSource;
cell.IsOpenContainer = cell.ItemId != 0 && cell.ItemId == open;
}
}
@ -533,8 +552,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
_disposed = true;
_objects.ObjectAdded -= OnObjectChanged;
_objects.ObjectMoved -= OnObjectMoved;
_objects.ObjectRemoved -= OnObjectChanged;
_objects.ObjectRemoved -= OnObjectRemoved;
_objects.ObjectUpdated -= OnObjectChanged;
_selection.Changed -= OnSelectionChanged;
if (_itemInteraction is not null)
_itemInteraction.StateChanged -= OnInteractionStateChanged;
}

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using AcDream.App.UI;
using AcDream.Core.Items;
using AcDream.Core.Selection;
namespace AcDream.App.UI.Layout;
@ -76,6 +77,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
private readonly Action<uint, uint>? _sendWield; // (itemGuid, equipMask) → GetAndWieldItem 0x001A
private readonly ItemInteractionController? _itemInteraction;
private readonly SelectionState _selection;
private readonly List<(EquipMask Mask, UiItemList List)> _slots = new();
// ── Slots-toggle state ────────────────────────────────────────────────────────────────────────
@ -86,11 +88,13 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
private PaperdollController(
ImportedLayout layout, ClientObjectTable objects, Func<uint> playerGuid,
Func<ItemType, uint, uint, uint, uint, uint> iconIds, Action<uint, uint>? sendWield,
Func<ItemType, uint, uint, uint, uint, uint> iconIds, SelectionState selection,
Action<uint, uint>? sendWield,
uint emptySlotSprite, UiDatFont? datFont, ItemInteractionController? itemInteraction)
{
_objects = objects; _playerGuid = playerGuid; _iconIds = iconIds; _sendWield = sendWield;
_itemInteraction = itemInteraction;
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
for (int i = 0; i < SlotMap.Length; i++)
{
@ -100,7 +104,16 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
list.Cell.SourceKind = ItemDragSource.Equipment;
list.Cell.SlotIndex = i; // SlotMap position = this equipped item's drag-payload SourceSlot when dragged out to unwield
list.Cell.EmptySprite = emptySlotSprite; // visible empty-slot frame so every position is seen + usable. The live
list.Cell.Clicked = () => { _itemInteraction?.AcquireSelfTarget(); };
list.Cell.Clicked = () =>
{
if (_itemInteraction?.IsTargetModeActive == true)
{
_itemInteraction.AcquireSelfTarget();
return;
}
if (list.Cell.ItemId != 0)
_selection.Select(list.Cell.ItemId, SelectionChangeSource.Paperdoll);
};
list.Cell.DoubleClicked = () =>
{
if (list.Cell.ItemId != 0)
@ -119,6 +132,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
_objects.ObjectMoved += OnObjectMoved;
_objects.ObjectRemoved += OnObjectChanged;
_objects.ObjectUpdated += OnObjectChanged;
_selection.Changed += OnSelectionChanged;
// ── Slots-toggle wiring ───────────────────────────────────────────────────────────────────
foreach (var id in ArmorSlotElementIds)
@ -154,14 +168,18 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
public static PaperdollController Bind(
ImportedLayout layout, ClientObjectTable objects, Func<uint> playerGuid,
Func<ItemType, uint, uint, uint, uint, uint> iconIds, Action<uint, uint>? sendWield = null,
Func<ItemType, uint, uint, uint, uint, uint> iconIds, SelectionState selection,
Action<uint, uint>? sendWield = null,
uint emptySlotSprite = 0u, UiDatFont? datFont = null,
ItemInteractionController? itemInteraction = null)
=> new PaperdollController(layout, objects, playerGuid, iconIds, sendWield, emptySlotSprite, datFont, itemInteraction);
=> new PaperdollController(
layout, objects, playerGuid, iconIds, selection, sendWield, emptySlotSprite,
datFont, itemInteraction);
private void OnObjectChanged(ClientObject o) { if (Concerns(o)) Populate(); }
private void OnObjectMoved(ClientObject o, uint from, uint to)
{ if (Concerns(o) || from == _playerGuid() || to == _playerGuid()) Populate(); }
private void OnSelectionChanged(SelectionTransition _) => ApplySelectionIndicators();
/// <summary>The object belongs to the player (wielded gear or pack contents) — so a change to it may
/// add/remove/repaint a doll slot. Player-scoped: an NPC's or vendor's wielded item (which also carries
@ -196,6 +214,16 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
uint tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects);
list.Cell.SetItem(worn.ObjectId, tex);
}
ApplySelectionIndicators();
}
private void ApplySelectionIndicators()
{
foreach (var (_, list) in _slots)
{
list.Cell.Selected = list.Cell.ItemId != 0
&& list.Cell.ItemId == _selection.SelectedObjectId;
}
}
private EquipMask MaskFor(UiItemList list)
@ -251,5 +279,6 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
_objects.ObjectMoved -= OnObjectMoved;
_objects.ObjectRemoved -= OnObjectChanged;
_objects.ObjectUpdated -= OnObjectChanged;
_selection.Changed -= OnSelectionChanged;
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Numerics;
using AcDream.App.UI;
using AcDream.Core.Selection;
namespace AcDream.App.UI.Layout;
@ -82,7 +83,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
private readonly Func<uint, bool> _hasHealth;
private readonly Func<uint, uint> _stackSize;
private readonly Action<uint> _sendQueryHealth;
private readonly Action<Action<uint?>> _unsubscribeSelectionChanged;
private readonly SelectionState _selection;
private readonly Action<Action<uint, float>> _unsubscribeHealthChanged;
// ── Live state (read by closures on the per-frame draw path) ────────────
@ -96,8 +97,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
private SelectedObjectController(
ImportedLayout layout,
Action<Action<uint?>> subscribeSelectionChanged,
Action<Action<uint?>> unsubscribeSelectionChanged,
SelectionState selection,
Action<Action<uint, float>> subscribeHealthChanged,
Action<Action<uint, float>> unsubscribeHealthChanged,
Func<uint, bool> isHealthTarget,
@ -114,7 +114,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
_hasHealth = hasHealth;
_stackSize = stackSize;
_sendQueryHealth = sendQueryHealth;
_unsubscribeSelectionChanged = unsubscribeSelectionChanged;
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
_unsubscribeHealthChanged = unsubscribeHealthChanged;
// Find elements — silently skip absent ones (partial/test layouts).
@ -171,8 +171,10 @@ public sealed class SelectedObjectController : IRetainedPanelController
}
// Register the handlers LAST so the initial state is fully set up first.
subscribeSelectionChanged(OnSelectionChanged);
_selection.Changed += OnSelectionTransition;
subscribeHealthChanged(OnHealthChanged);
if (_selection.SelectedObjectId is { } initial)
ApplySelection(initial);
}
/// <summary>
@ -180,8 +182,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
/// Port of retail <c>gmToolbarUI::HandleSelectionChanged</c> + <c>RecvNotice_UpdateObjectHealth</c>.
/// </summary>
/// <param name="layout">Imported toolbar layout (LayoutDesc 0x21000016).</param>
/// <param name="subscribeSelectionChanged">Called once with <see cref="OnSelectionChanged"/>
/// (typical host: <c>h =&gt; SelectionChanged += h</c>).</param>
/// <param name="selection">The single Core selected-object owner.</param>
/// <param name="subscribeHealthChanged">Called once with <see cref="OnHealthChanged"/>
/// (typical host: <c>h =&gt; Combat.HealthChanged += h</c>) — drives meter visibility.</param>
/// <param name="isHealthTarget">Returns true for guids that may show a health meter
@ -195,8 +196,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
/// <param name="datFont">Dat font for the name label; null = debug bitmap font fallback.</param>
public static SelectedObjectController Bind(
ImportedLayout layout,
Action<Action<uint?>> subscribeSelectionChanged,
Action<Action<uint?>> unsubscribeSelectionChanged,
SelectionState selection,
Action<Action<uint, float>> subscribeHealthChanged,
Action<Action<uint, float>> unsubscribeHealthChanged,
Func<uint, bool> isHealthTarget,
@ -207,7 +207,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
Action<uint> sendQueryHealth,
UiDatFont? datFont)
=> new SelectedObjectController(
layout, subscribeSelectionChanged, unsubscribeSelectionChanged,
layout, selection,
subscribeHealthChanged, unsubscribeHealthChanged,
isHealthTarget, name, healthPercent, hasHealth, stackSize, sendQueryHealth, datFont);
@ -215,7 +215,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
/// Port of <c>gmToolbarUI::HandleSelectionChanged</c> (<c>:198635</c>):
/// clear-then-populate the selected-object strip on any selection change.
/// </summary>
public void OnSelectionChanged(uint? guid)
private void ApplySelection(uint? guid)
{
// ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0)
// + SetVisible(0) on the meters). ──────────────────────────────────────
@ -279,11 +279,14 @@ public sealed class SelectedObjectController : IRetainedPanelController
_overlay?.TrySetRetailState(state);
}
private void OnSelectionTransition(SelectionTransition transition)
=> ApplySelection(transition.SelectedObjectId);
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_unsubscribeSelectionChanged(OnSelectionChanged);
_selection.Changed -= OnSelectionTransition;
_unsubscribeHealthChanged(OnHealthChanged);
}
}

View file

@ -6,6 +6,7 @@ using AcDream.App.UI.Testing;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
using AcDream.Core.Selection;
using AcDream.UI.Abstractions;
using AcDream.UI.Abstractions.Panels.Chat;
using AcDream.UI.Abstractions.Panels.Settings;
@ -31,7 +32,7 @@ public sealed record ChatRuntimeBindings(ChatVM ViewModel, Func<ICommandBus> Com
public sealed record RadarRuntimeBindings(
Func<UiRadarSnapshot> Snapshot,
Action<uint> SelectObject,
SelectionState Selection,
Action<bool> SetUiLocked);
public sealed record ToolbarRuntimeBindings(
@ -43,8 +44,7 @@ public sealed record ToolbarRuntimeBindings(
ItemInteractionController ItemInteraction,
Action<uint, uint>? SendAddShortcut,
Action<uint>? SendRemoveShortcut,
Action<Action<uint?>> SubscribeSelectionChanged,
Action<Action<uint?>> UnsubscribeSelectionChanged,
SelectionState Selection,
Action<Action<uint, float>> SubscribeHealthChanged,
Action<Action<uint, float>> UnsubscribeHealthChanged,
Func<uint, bool> IsHealthTarget,
@ -65,7 +65,8 @@ public sealed record InventoryRuntimeBindings(
Action<uint>? SendNoLongerViewing,
Action<uint, uint, int>? SendPutItemInContainer,
Action<uint, uint>? SendWield,
ItemInteractionController ItemInteraction);
ItemInteractionController ItemInteraction,
SelectionState Selection);
public sealed record RetailUiPersistenceBindings(
SettingsStore Store,
@ -256,7 +257,7 @@ public sealed class RetailUiRuntime : IDisposable
RadarController controller = RadarController.Bind(
layout,
_bindings.Radar.Snapshot,
_bindings.Radar.SelectObject,
guid => _bindings.Radar.Selection.Select(guid, SelectionChangeSource.Radar),
setUiLocked: _bindings.Radar.SetUiLocked,
datFont: _bindings.Assets.DefaultFont);
RetailWindowFrame.Mount(Host.Root, radarRoot, _bindings.Assets.ResolveSprite,
@ -354,8 +355,7 @@ public sealed class RetailUiRuntime : IDisposable
peace, war, empty, b.ItemInteraction, b.SendAddShortcut, b.SendRemoveShortcut);
SelectedObjectController = Layout.SelectedObjectController.Bind(
layout,
b.SubscribeSelectionChanged,
b.UnsubscribeSelectionChanged,
b.Selection,
b.SubscribeHealthChanged,
b.UnsubscribeHealthChanged,
b.IsHealthTarget,
@ -570,13 +570,13 @@ public sealed class RetailUiRuntime : IDisposable
InventoryRuntimeBindings b = _bindings.Inventory;
InventoryController inventory = InventoryController.Bind(
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Strength,
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Strength, b.Selection,
_bindings.Assets.DefaultFont, _bindings.Character.Provider.CharacterName,
contents, sideBag, mainPack, b.SendUse, b.SendNoLongerViewing,
b.SendPutItemInContainer, b.ItemInteraction,
() => CloseWindow(WindowNames.Inventory));
PaperdollController paperdoll = PaperdollController.Bind(
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.SendWield,
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.SendWield,
contents, _bindings.Assets.DefaultFont, b.ItemInteraction);
Host.WindowManager.AttachController(
WindowNames.Inventory,

View file

@ -24,7 +24,7 @@ namespace AcDream.App.UI;
/// on internal state types:
/// </para>
/// <list type="bullet">
/// <item><c>selectedGuidProvider</c> — host's current <c>_selectedGuid</c>.</item>
/// <item><c>selectedGuidProvider</c> — Core <c>SelectionState</c>'s current guid.</item>
/// <item><c>entityResolver</c> — returns
/// <see cref="TargetInfo"/> for a given guid, or <c>null</c> if
/// the entity is no longer in the world (despawned).</item>