Add the ClientUISystem ground-object lifecycle, authoritative root and nested ViewContents projections, replacement and close semantics, and the DAT-authored gmExternalContainerUI strip for chests and corpses. Route double-click loot and full or partial drag transfers through the shared retail item policy without optimistic external ownership. Remove the incorrect NoLongerViewingContents behavior from owned side packs and retire AP-106/#196. Release build succeeds and all 5,875 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
133 lines
4 KiB
C#
133 lines
4 KiB
C#
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);
|
|
|
|
/// <summary>
|
|
/// Single owner for the client's selected object. This ports the state behavior
|
|
/// of retail <c>ACCWeenieObject::SetSelectedObject @ 0x0058C2E0</c>: changes are
|
|
/// deduplicated, the old selection becomes the previous selection, and plugin/UI
|
|
/// listeners receive one transition after state is committed.
|
|
/// </summary>
|
|
public sealed class SelectionState : ISelectionService
|
|
{
|
|
private event Action<SelectionChangedEvent>? PluginChanged;
|
|
|
|
public uint? SelectedObjectId { get; private set; }
|
|
public uint? PreviousObjectId { get; private set; }
|
|
public uint? PreviousValidObjectId { get; private set; }
|
|
|
|
public event Action<SelectionTransition>? Changed;
|
|
|
|
event Action<SelectionChangedEvent> 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);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<SelectionChangedEvent>? listeners = PluginChanged;
|
|
if (listeners is null) return;
|
|
foreach (Action<SelectionChangedEvent> listener in listeners.GetInvocationList())
|
|
{
|
|
try { listener(change); }
|
|
catch { /* plugin exceptions never escape into client state changes */ }
|
|
}
|
|
}
|
|
}
|