refactor(app): compose interaction and retained UI startup
This commit is contained in:
parent
f663b04a54
commit
aa6ffa5176
15 changed files with 2157 additions and 467 deletions
479
src/AcDream.App/Composition/InteractionUiRuntimeSources.cs
Normal file
479
src/AcDream.App/Composition/InteractionUiRuntimeSources.cs
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.App.UI.Testing;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.UI.Abstractions;
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Early UI view over the later live-session owner. A binding lease clears only
|
||||
/// the exact owner it installed, so rollback cannot withdraw a replacement.
|
||||
/// </summary>
|
||||
internal sealed class DeferredLiveSessionUiAuthority
|
||||
: ILiveInWorldSource,
|
||||
ILiveWorldSessionSource
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private ILiveUiSessionTarget? _target;
|
||||
private bool _deactivated;
|
||||
|
||||
public bool IsInWorld
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
return !_deactivated && _target?.IsInWorld == true;
|
||||
}
|
||||
}
|
||||
|
||||
public WorldSession? CurrentSession
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
return !_deactivated ? _target?.CurrentSession : null;
|
||||
}
|
||||
}
|
||||
|
||||
public ICommandBus Commands
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
return !_deactivated
|
||||
? _target?.Commands ?? NullCommandBus.Instance
|
||||
: NullCommandBus.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
public string? AccountName => CurrentSession?.Characters?.AccountName;
|
||||
|
||||
public LinkStatusSnapshot LinkStatus =>
|
||||
CurrentSession?.LinkStatus ?? LinkStatusSnapshot.Disconnected;
|
||||
|
||||
public IDisposable Bind(ILiveUiSessionTarget target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||
if (_target is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The retained-UI live-session authority is already bound.");
|
||||
}
|
||||
|
||||
_target = target;
|
||||
}
|
||||
|
||||
return new ExpectedOwnerBinding<ILiveUiSessionTarget>(
|
||||
target,
|
||||
Release);
|
||||
}
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_deactivated = true;
|
||||
_target = null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryUseItem(uint guid, Action<string>? log = null)
|
||||
{
|
||||
WorldSession? session = CurrentSession;
|
||||
if (!IsInWorld || session is null)
|
||||
return false;
|
||||
|
||||
uint sequence = session.NextGameActionSequence();
|
||||
session.SendGameAction(InteractRequests.BuildUse(sequence, guid));
|
||||
log?.Invoke($"[D.5.1] toolbar use-item guid=0x{guid:X8} seq={sequence}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Release(ILiveUiSessionTarget expected)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (ReferenceEquals(_target, expected))
|
||||
_target = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selection operations mounted by retained UI before the Phase-6 world query
|
||||
/// and interaction controller exist.
|
||||
/// </summary>
|
||||
internal sealed class DeferredSelectionUiAuthority
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private IRetainedUiSelectionQuery? _query;
|
||||
private SelectionInteractionController? _interactions;
|
||||
private bool _deactivated;
|
||||
|
||||
public IDisposable Bind(
|
||||
IRetainedUiSelectionQuery query,
|
||||
SelectionInteractionController interactions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
ArgumentNullException.ThrowIfNull(interactions);
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||
if (_query is not null || _interactions is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The retained-UI selection authority is already bound.");
|
||||
}
|
||||
|
||||
_query = query;
|
||||
_interactions = interactions;
|
||||
}
|
||||
|
||||
return new ExpectedSelectionBinding(this, query, interactions);
|
||||
}
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_deactivated = true;
|
||||
_query = null;
|
||||
_interactions = null;
|
||||
}
|
||||
}
|
||||
|
||||
public uint? PickAtCursor(bool includeSelf)
|
||||
{
|
||||
SelectionInteractionController? target;
|
||||
lock (_gate)
|
||||
target = !_deactivated ? _interactions : null;
|
||||
return target?.PickAtCursor(includeSelf);
|
||||
}
|
||||
|
||||
public void SendUse(uint guid)
|
||||
{
|
||||
SelectionInteractionController? target;
|
||||
lock (_gate)
|
||||
target = !_deactivated ? _interactions : null;
|
||||
target?.SendUse(guid);
|
||||
}
|
||||
|
||||
public void SendPickup(uint itemGuid, uint destinationContainerId, int placement)
|
||||
{
|
||||
SelectionInteractionController? target;
|
||||
lock (_gate)
|
||||
target = !_deactivated ? _interactions : null;
|
||||
target?.SendPickup(itemGuid, destinationContainerId, placement);
|
||||
}
|
||||
|
||||
public uint? SelectClosestCombatTarget(bool showToast)
|
||||
{
|
||||
SelectionInteractionController? target;
|
||||
lock (_gate)
|
||||
target = !_deactivated ? _interactions : null;
|
||||
return target?.SelectClosestCombatTarget(showToast);
|
||||
}
|
||||
|
||||
public bool IsWithinExternalContainerUseRange(uint targetGuid)
|
||||
{
|
||||
IRetainedUiSelectionQuery? query;
|
||||
lock (_gate)
|
||||
query = !_deactivated ? _query : null;
|
||||
return query?.IsWithinExternalContainerUseRange(targetGuid) != false;
|
||||
}
|
||||
|
||||
public bool ShouldShowHealth(uint guid)
|
||||
{
|
||||
IRetainedUiSelectionQuery? query;
|
||||
lock (_gate)
|
||||
query = !_deactivated ? _query : null;
|
||||
return query?.ShouldShowHealth(guid) == true;
|
||||
}
|
||||
|
||||
public VividTargetInfo? ResolveVividTargetInfo(uint guid)
|
||||
{
|
||||
IRetainedUiSelectionQuery? query;
|
||||
lock (_gate)
|
||||
query = !_deactivated ? _query : null;
|
||||
return query?.ResolveVividTargetInfo(guid);
|
||||
}
|
||||
|
||||
private void Release(
|
||||
IRetainedUiSelectionQuery expectedQuery,
|
||||
SelectionInteractionController expectedInteractions)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (ReferenceEquals(_query, expectedQuery)
|
||||
&& ReferenceEquals(_interactions, expectedInteractions))
|
||||
{
|
||||
_query = null;
|
||||
_interactions = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ExpectedSelectionBinding : IDisposable
|
||||
{
|
||||
private DeferredSelectionUiAuthority? _owner;
|
||||
private readonly IRetainedUiSelectionQuery _query;
|
||||
private readonly SelectionInteractionController _interactions;
|
||||
|
||||
public ExpectedSelectionBinding(
|
||||
DeferredSelectionUiAuthority owner,
|
||||
IRetainedUiSelectionQuery query,
|
||||
SelectionInteractionController interactions)
|
||||
{
|
||||
_owner = owner;
|
||||
_query = query;
|
||||
_interactions = interactions;
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.Release(_query, _interactions);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Identity view plane until Phase 7 binds portal presentation.</summary>
|
||||
internal sealed class DeferredSelectionViewPlaneSource
|
||||
{
|
||||
private ISelectionViewPlaneSource? _target;
|
||||
private bool _deactivated;
|
||||
|
||||
public IDisposable Bind(ISelectionViewPlaneSource target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||
if (_target is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The retained-UI selection view plane is already bound.");
|
||||
}
|
||||
|
||||
_target = target;
|
||||
return new ExpectedOwnerBinding<ISelectionViewPlaneSource>(target, Release);
|
||||
}
|
||||
|
||||
public ICamera Apply(ICamera camera) =>
|
||||
!_deactivated && _target is { } target
|
||||
? target.ApplyViewPlane(camera)
|
||||
: camera;
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
_deactivated = true;
|
||||
_target = null;
|
||||
}
|
||||
|
||||
private void Release(ISelectionViewPlaneSource expected)
|
||||
{
|
||||
if (ReferenceEquals(_target, expected))
|
||||
_target = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SelectionCameraSource
|
||||
{
|
||||
private readonly CameraController _camera;
|
||||
private readonly IView _window;
|
||||
private readonly DeferredSelectionViewPlaneSource _viewPlane;
|
||||
|
||||
public SelectionCameraSource(
|
||||
CameraController camera,
|
||||
IView window,
|
||||
DeferredSelectionViewPlaneSource viewPlane)
|
||||
{
|
||||
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||
_window = window ?? throw new ArgumentNullException(nameof(window));
|
||||
_viewPlane = viewPlane ?? throw new ArgumentNullException(nameof(viewPlane));
|
||||
}
|
||||
|
||||
public SelectionCameraSnapshot Snapshot()
|
||||
{
|
||||
ICamera camera = _viewPlane.Apply(_camera.Active);
|
||||
return new SelectionCameraSnapshot(
|
||||
camera.View,
|
||||
camera.Projection,
|
||||
new Vector2(_window.Size.X, _window.Size.Y));
|
||||
}
|
||||
|
||||
public (Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport) UiSnapshot()
|
||||
{
|
||||
SelectionCameraSnapshot snapshot = Snapshot();
|
||||
return (snapshot.View, snapshot.Projection, snapshot.Viewport);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DeferredRadarSnapshotSource
|
||||
{
|
||||
private RadarSnapshotProvider? _target;
|
||||
private bool _deactivated;
|
||||
|
||||
public UiRadarSnapshot Snapshot() =>
|
||||
!_deactivated && _target is { } target
|
||||
? target.BuildSnapshot()
|
||||
: UiRadarSnapshot.Empty;
|
||||
|
||||
public IDisposable Bind(RadarSnapshotProvider target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||
if (_target is not null)
|
||||
throw new InvalidOperationException("The retained-UI radar is already bound.");
|
||||
_target = target;
|
||||
return new ExpectedOwnerBinding<RadarSnapshotProvider>(target, Release);
|
||||
}
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
_deactivated = true;
|
||||
_target = null;
|
||||
}
|
||||
|
||||
private void Release(RadarSnapshotProvider expected)
|
||||
{
|
||||
if (ReferenceEquals(_target, expected))
|
||||
_target = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DeferredInventoryContainerSource
|
||||
{
|
||||
private RetailUiRuntime? _runtime;
|
||||
private bool _deactivated;
|
||||
|
||||
public uint Current(uint playerGuid) =>
|
||||
!_deactivated
|
||||
? _runtime?.InventoryPanelController?.CurrentOpenContainerId ?? playerGuid
|
||||
: playerGuid;
|
||||
|
||||
public IDisposable Bind(RetailUiRuntime runtime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||
if (_runtime is not null)
|
||||
throw new InvalidOperationException("The retained inventory container is already bound.");
|
||||
_runtime = runtime;
|
||||
return new ExpectedOwnerBinding<RetailUiRuntime>(runtime, Release);
|
||||
}
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
_deactivated = true;
|
||||
_runtime = null;
|
||||
}
|
||||
|
||||
private void Release(RetailUiRuntime expected)
|
||||
{
|
||||
if (ReferenceEquals(_runtime, expected))
|
||||
_runtime = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PlayerCharacterOptionsState
|
||||
{
|
||||
public PlayerDescriptionParser.CharacterOptions1 Options { get; set; } =
|
||||
PlayerDescriptionParser.CharacterOptions1.Default;
|
||||
|
||||
public bool DragItemOnPlayerOpensSecureTrade =>
|
||||
(Options
|
||||
& PlayerDescriptionParser.CharacterOptions1
|
||||
.DragItemOnPlayerOpensSecureTrade) != 0;
|
||||
|
||||
public void Reset() => Options = PlayerDescriptionParser.CharacterOptions1.Default;
|
||||
}
|
||||
|
||||
internal sealed class ShortcutSnapshotState
|
||||
{
|
||||
private IReadOnlyList<AcDream.Core.Items.ShortcutEntry> _items =
|
||||
Array.Empty<AcDream.Core.Items.ShortcutEntry>();
|
||||
|
||||
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> Items
|
||||
{
|
||||
get => _items;
|
||||
set => _items = value ?? Array.Empty<AcDream.Core.Items.ShortcutEntry>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Probe scripts are mounted in Phase 5; reveal/resource facts become valid in
|
||||
/// Phase 7. Calls remain inert and diagnostic until that exact owner binds.
|
||||
/// </summary>
|
||||
internal sealed class DeferredWorldLifecycleAutomationRuntime
|
||||
: IRetailUiAutomationRuntime
|
||||
{
|
||||
private IRetailUiAutomationRuntime? _target;
|
||||
private bool _deactivated;
|
||||
|
||||
public bool IsWorldReady => !_deactivated && _target?.IsWorldReady == true;
|
||||
public bool IsWorldViewportVisible =>
|
||||
!_deactivated && _target?.IsWorldViewportVisible == true;
|
||||
public int PortalMaterializationCount =>
|
||||
!_deactivated ? _target?.PortalMaterializationCount ?? 0 : 0;
|
||||
|
||||
public IDisposable Bind(IRetailUiAutomationRuntime target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||
if (_target is not null)
|
||||
throw new InvalidOperationException("World lifecycle automation is already bound.");
|
||||
_target = target;
|
||||
return new ExpectedOwnerBinding<IRetailUiAutomationRuntime>(target, Release);
|
||||
}
|
||||
|
||||
public bool TryWriteCheckpoint(string name, out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryWriteCheckpoint(name, out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryRequestScreenshot(string name, out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryRequestScreenshot(name, out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsScreenshotComplete(string name) =>
|
||||
!_deactivated && _target?.IsScreenshotComplete(name) == true;
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
_deactivated = true;
|
||||
_target = null;
|
||||
}
|
||||
|
||||
private void Release(IRetailUiAutomationRuntime expected)
|
||||
{
|
||||
if (ReferenceEquals(_target, expected))
|
||||
_target = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ExpectedOwnerBinding<T> : IDisposable where T : class
|
||||
{
|
||||
private Action<T>? _release;
|
||||
private readonly T _expected;
|
||||
|
||||
public ExpectedOwnerBinding(T expected, Action<T> release)
|
||||
{
|
||||
_expected = expected ?? throw new ArgumentNullException(nameof(expected));
|
||||
_release = release ?? throw new ArgumentNullException(nameof(release));
|
||||
}
|
||||
|
||||
public void Dispose() => Interlocked.Exchange(ref _release, null)?.Invoke(_expected);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue