using AcDream.Plugin.Abstractions; namespace AcDream.Core.Selection; public enum SelectionChangeSource { System, World, Radar, Inventory, ExternalContainer, Paperdoll, Toolbar, Keyboard, Plugin, } public enum SelectionChangeReason { Selected, Cleared, SelectedObjectRemoved, CombatTargetDied, PreviousSelection, SessionReset, } public readonly record struct SelectionTransition( uint? PreviousObjectId, uint? SelectedObjectId, SelectionChangeSource Source, SelectionChangeReason Reason); /// /// Single owner for the client's selected object. This ports the state behavior /// of retail ACCWeenieObject::SetSelectedObject @ 0x0058C2E0: changes are /// deduplicated, the old selection becomes the previous selection, and plugin/UI /// listeners receive one transition after state is committed. /// public sealed class SelectionState : ISelectionService { private event Action? PluginChanged; public uint? SelectedObjectId { get; private set; } public uint? PreviousObjectId { get; private set; } public uint? PreviousValidObjectId { get; private set; } public event Action? Changed; event Action ISelectionService.Changed { add => PluginChanged += value; remove => PluginChanged -= value; } public bool Select( uint objectId, SelectionChangeSource source, SelectionChangeReason reason = SelectionChangeReason.Selected) => objectId == 0 ? Clear(source) : Set(objectId, source, reason); public bool Clear( SelectionChangeSource source, SelectionChangeReason reason = SelectionChangeReason.Cleared) => Set(null, source, reason); public bool SelectPrevious(SelectionChangeSource source = SelectionChangeSource.Keyboard) => PreviousObjectId is uint previous && Set(previous, source, SelectionChangeReason.PreviousSelection); /// /// Ends a world session. Unlike an ordinary selection clear, no previous /// object survives for the next session: server GUIDs may be reused by a /// different logical incarnation after reconnect. /// public bool Reset(SelectionChangeSource source = SelectionChangeSource.System) { uint? old = SelectedObjectId; SelectedObjectId = null; PreviousObjectId = null; PreviousValidObjectId = null; if (old is null) return false; var transition = new SelectionTransition( old, null, source, SelectionChangeReason.SessionReset); Changed?.Invoke(transition); DispatchPluginChanged(new SelectionChangedEvent(old, null)); return true; } bool ISelectionService.Select(uint objectId) => Select(objectId, SelectionChangeSource.Plugin); bool ISelectionService.Clear() => Clear(SelectionChangeSource.Plugin); private bool Set( uint? selectedObjectId, SelectionChangeSource source, SelectionChangeReason reason) { if (SelectedObjectId == selectedObjectId) return false; uint? old = SelectedObjectId; SelectedObjectId = selectedObjectId; PreviousObjectId = old; if (old is not null) PreviousValidObjectId = old; var transition = new SelectionTransition(old, selectedObjectId, source, reason); Changed?.Invoke(transition); DispatchPluginChanged(new SelectionChangedEvent(old, selectedObjectId)); return true; } private void DispatchPluginChanged(SelectionChangedEvent change) { Action? listeners = PluginChanged; if (listeners is null) return; foreach (Action listener in listeners.GetInvocationList()) { try { listener(change); } catch { /* plugin exceptions never escape into client state changes */ } } } }