using AcDream.Plugin.Abstractions; namespace AcDream.Core.Selection; public enum SelectionChangeSource { System, World, Radar, Inventory, ExternalContainer, Paperdoll, Toolbar, Keyboard, Plugin, // Slice 6.2: a vendor shop-list row click — research doc §B.4 confirms // vendor-context selections flow through the SAME global // ACCWeenieObject::SetSelectedObject primitive as every other origin // (VendorSellUI::AddItemToSell, pc:203558), never a vendor-local one. Vendor, } 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; bool changed = old is not null || PreviousObjectId is not null || PreviousValidObjectId is not null; SelectedObjectId = null; PreviousObjectId = null; PreviousValidObjectId = null; var transition = new SelectionTransition( old, null, source, SelectionChangeReason.SessionReset); List? failures = DispatchChanged(transition); DispatchPluginChanged(new SelectionChangedEvent(old, null)); if (failures is not null) throw new AggregateException( "One or more selection reset observers failed.", failures); return changed; } 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 List? DispatchChanged(SelectionTransition transition) { Action? listeners = Changed; if (listeners is null) return null; List? failures = null; foreach (Action listener in listeners.GetInvocationList()) { try { listener(transition); } catch (Exception error) { (failures ??= []).Add(error); } } return failures; } 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 */ } } } }