feat(ui): centralize retail selection state
This commit is contained in:
parent
c7607f019c
commit
7983309d23
30 changed files with 591 additions and 108 deletions
|
|
@ -16,6 +16,7 @@ public sealed class GameWindow : IDisposable
|
|||
private readonly string _datDir;
|
||||
private readonly WorldGameState _worldGameState;
|
||||
private readonly WorldEvents _worldEvents;
|
||||
private readonly AcDream.Core.Selection.SelectionState _selection;
|
||||
private IWindow? _window;
|
||||
private GL? _gl;
|
||||
private IInputContext? _input;
|
||||
|
|
@ -1032,24 +1033,6 @@ public sealed class GameWindow : IDisposable
|
|||
/// fields when a 0xF625 ObjDescEvent arrives carrying only updated visuals.
|
||||
/// </summary>
|
||||
private readonly Dictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> _lastSpawnByGuid = new();
|
||||
// Current selection: written by Q-cycle (combat) and LMB click (interact); cleared on entity despawn.
|
||||
private uint? _selectedGuid;
|
||||
/// <summary>Fires when the selected world object changes (retail gmToolbarUI selection-change event,
|
||||
/// acclient_2013_pseudo_c.txt:198635). Private: only the internal SelectedObjectController subscribes.</summary>
|
||||
private event Action<uint?>? SelectionChanged;
|
||||
/// <summary>Currently-selected world object guid. The setter fires <see cref="SelectionChanged"/> only on
|
||||
/// an actual change (dedup), so all writes go through here; reads may use the field directly.</summary>
|
||||
private uint? SelectedGuid
|
||||
{
|
||||
get => _selectedGuid;
|
||||
set
|
||||
{
|
||||
if (_selectedGuid == value) return;
|
||||
_selectedGuid = value;
|
||||
SelectionChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
// B.6/B.7 (2026-05-16): pending close-range action that will be fired
|
||||
// once the local auto-walk overlay reports arrival (body has finished
|
||||
// rotating to face the target). Only set for close-range Use/PickUp;
|
||||
|
|
@ -1085,13 +1068,18 @@ public sealed class GameWindow : IDisposable
|
|||
private int _liveAnimRejectSingleFrame;
|
||||
private int _liveAnimRejectPartFrames;
|
||||
|
||||
public GameWindow(AcDream.App.RuntimeOptions options, WorldGameState worldGameState, WorldEvents worldEvents,
|
||||
public GameWindow(
|
||||
AcDream.App.RuntimeOptions options,
|
||||
WorldGameState worldGameState,
|
||||
WorldEvents worldEvents,
|
||||
AcDream.Core.Selection.SelectionState selection,
|
||||
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null)
|
||||
{
|
||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||
_datDir = options.DatDir;
|
||||
_worldGameState = worldGameState;
|
||||
_worldEvents = worldEvents;
|
||||
_selection = selection ?? throw new System.ArgumentNullException(nameof(selection));
|
||||
_uiRegistry = uiRegistry;
|
||||
SpellBook = new AcDream.Core.Spells.Spellbook(SpellTable);
|
||||
LocalPlayer = new AcDream.Core.Player.LocalPlayerState(SpellBook);
|
||||
|
|
@ -1525,13 +1513,13 @@ public sealed class GameWindow : IDisposable
|
|||
// B.7 Vivid Target Indicator — corner-triangle highlight
|
||||
// around the currently-selected entity. Delegates pull
|
||||
// live state from this GameWindow instance every frame:
|
||||
// - selected guid → _selectedGuid (set by PickAndStoreSelection)
|
||||
// - selected guid → shared SelectionState
|
||||
// - entity resolver → position from _entitiesByServerGuid +
|
||||
// itemType from ClientObjectTable (Objects) + last spawn
|
||||
// - camera → _cameraController.Active or (zero) when not
|
||||
// yet ready, in which case the panel bails on viewport==0.
|
||||
_targetIndicator = new AcDream.App.UI.TargetIndicatorPanel(
|
||||
selectedGuidProvider: () => _selectedGuid,
|
||||
selectedGuidProvider: () => _selection.SelectedObjectId,
|
||||
entityResolver: guid =>
|
||||
{
|
||||
if (!_entitiesByServerGuid.TryGetValue(guid, out var entity))
|
||||
|
|
@ -2078,7 +2066,7 @@ public sealed class GameWindow : IDisposable
|
|||
playerGuid: () => _playerServerGuid,
|
||||
playerYawRadians: () => _playerController?.Yaw ?? 0f,
|
||||
playerCellId: () => _playerController?.CellId ?? 0u,
|
||||
selectedGuid: () => _selectedGuid,
|
||||
selectedGuid: () => _selection.SelectedObjectId,
|
||||
coordinatesOnRadar: () => _persistedGameplay.CoordinatesOnRadar,
|
||||
uiLocked: () => _persistedGameplay.LockUI);
|
||||
var retailChatVm = new AcDream.UI.Abstractions.Panels.Chat.ChatVM(Chat, displayLimit: 200);
|
||||
|
|
@ -2103,7 +2091,7 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.UI.Abstractions.NullCommandBus.Instance),
|
||||
Radar: new AcDream.App.UI.RadarRuntimeBindings(
|
||||
radarSnapshotProvider.BuildSnapshot,
|
||||
guid => SelectedGuid = guid,
|
||||
_selection,
|
||||
SetRetailUiLocked),
|
||||
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
|
||||
Objects,
|
||||
|
|
@ -2114,8 +2102,7 @@ public sealed class GameWindow : IDisposable
|
|||
_itemInteractionController,
|
||||
(index, guid) => _liveSession?.SendAddShortcut(index, guid),
|
||||
index => _liveSession?.SendRemoveShortcut(index),
|
||||
handler => SelectionChanged += handler,
|
||||
handler => SelectionChanged -= handler,
|
||||
_selection,
|
||||
handler => Combat.HealthChanged += handler,
|
||||
handler => Combat.HealthChanged -= handler,
|
||||
IsHealthBarTarget,
|
||||
|
|
@ -2137,7 +2124,8 @@ public sealed class GameWindow : IDisposable
|
|||
(item, container, placement) =>
|
||||
_liveSession?.SendPutItemInContainer(item, container, placement),
|
||||
(item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
|
||||
_itemInteractionController),
|
||||
_itemInteractionController,
|
||||
_selection),
|
||||
Cursor: new AcDream.App.UI.RetailUiCursorBindings(
|
||||
cursorFeedbackController,
|
||||
retailCursorManager),
|
||||
|
|
@ -4478,8 +4466,12 @@ public sealed class GameWindow : IDisposable
|
|||
_remoteLastMove.Remove(serverGuid);
|
||||
_entitiesByServerGuid.Remove(serverGuid);
|
||||
_lastSpawnByGuid.Remove(serverGuid);
|
||||
if (_selectedGuid == serverGuid)
|
||||
SelectedGuid = null;
|
||||
if (_selection.SelectedObjectId == serverGuid)
|
||||
{
|
||||
_selection.Clear(
|
||||
AcDream.Core.Selection.SelectionChangeSource.System,
|
||||
AcDream.Core.Selection.SelectionChangeReason.SelectedObjectRemoved);
|
||||
}
|
||||
|
||||
// A7 indoor lighting: release this entity's owned lights on EVERY
|
||||
// removal, including the respawn-dedup path (former logDelete=false).
|
||||
|
|
@ -11874,6 +11866,10 @@ public sealed class GameWindow : IDisposable
|
|||
SelectClosestCombatTarget(showToast: true);
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.SelectionPreviousSelection:
|
||||
_selection.SelectPrevious();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.CombatToggleCombat:
|
||||
ToggleLiveCombatMode();
|
||||
break;
|
||||
|
|
@ -11903,7 +11899,7 @@ public sealed class GameWindow : IDisposable
|
|||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.SelectionPickUp:
|
||||
if (_selectedGuid is uint pickupTarget)
|
||||
if (_selection.SelectedObjectId is uint pickupTarget)
|
||||
SendPickUp(pickupTarget);
|
||||
else
|
||||
_debugVm?.AddToast("Nothing selected");
|
||||
|
|
@ -11979,7 +11975,7 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
private uint? GetSelectedOrClosestCombatTarget()
|
||||
{
|
||||
if (_selectedGuid is { } selected && IsLiveCreatureTarget(selected))
|
||||
if (_selection.SelectedObjectId is { } selected && IsLiveCreatureTarget(selected))
|
||||
return selected;
|
||||
|
||||
return SelectClosestCombatTarget(showToast: false);
|
||||
|
|
@ -12066,7 +12062,7 @@ public sealed class GameWindow : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
SelectedGuid = guid;
|
||||
_selection.Select(guid, AcDream.Core.Selection.SelectionChangeSource.World);
|
||||
string label = DescribeLiveEntity(guid);
|
||||
Console.WriteLine($"[B.4b] pick guid=0x{guid:X8} name={label}");
|
||||
// B.7 (2026-05-15): one-shot per-pick diagnostic so we can
|
||||
|
|
@ -12107,7 +12103,7 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
private void UseCurrentSelection()
|
||||
{
|
||||
if (_selectedGuid is not uint sel)
|
||||
if (_selection.SelectedObjectId is not uint sel)
|
||||
{
|
||||
_debugVm?.AddToast("Nothing selected");
|
||||
return;
|
||||
|
|
@ -12503,7 +12499,10 @@ public sealed class GameWindow : IDisposable
|
|||
bestGuid = guid;
|
||||
}
|
||||
|
||||
SelectedGuid = bestGuid;
|
||||
if (bestGuid is { } selection)
|
||||
_selection.Select(selection, AcDream.Core.Selection.SelectionChangeSource.Keyboard);
|
||||
else
|
||||
_selection.Clear(AcDream.Core.Selection.SelectionChangeSource.Keyboard);
|
||||
if (bestGuid is { } selected)
|
||||
{
|
||||
string label = DescribeLiveEntity(selected);
|
||||
|
|
@ -13592,6 +13591,8 @@ public sealed class GameWindow : IDisposable
|
|||
_retailUiRuntime?.Dispose();
|
||||
_retailUiRuntime = null;
|
||||
_uiHost = null;
|
||||
_itemInteractionController?.Dispose();
|
||||
_itemInteractionController = null;
|
||||
|
||||
// Phase A.1: join the streamer worker thread before tearing down GL
|
||||
// state. The worker may still be processing a load job that references
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue