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
832
src/AcDream.App/Composition/InteractionRetainedUiComposition.cs
Normal file
832
src/AcDream.App/Composition/InteractionRetainedUiComposition.cs
Normal file
|
|
@ -0,0 +1,832 @@
|
|||
using System.Collections.Concurrent;
|
||||
using AcDream.App.Combat;
|
||||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.App.Plugins;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Settings;
|
||||
using AcDream.App.Spells;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Player;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
using AcDream.UI.Abstractions.Panels.Vitals;
|
||||
using DatReaderWriter;
|
||||
using Silk.NET.Input;
|
||||
using Silk.NET.OpenGL;
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
||||
internal sealed record InteractionRetainedUiDependencies(
|
||||
RuntimeOptions Options,
|
||||
GL Gl,
|
||||
IView Window,
|
||||
IInputContext Input,
|
||||
string ShadersDirectory,
|
||||
IDatReaderWriter Dats,
|
||||
object DatLock,
|
||||
TextureCache TextureCache,
|
||||
BitmapFont? DebugFont,
|
||||
HostQuiescenceGate HostQuiescence,
|
||||
RetainedUiInputCaptureSlot RetainedInputCapture,
|
||||
InputDispatcher? InputDispatcher,
|
||||
RuntimeSettingsController Settings,
|
||||
CombatState Combat,
|
||||
ICombatAttackOperations CombatAttackOperations,
|
||||
SelectionState Selection,
|
||||
ExternalContainerState ExternalContainers,
|
||||
ClientObjectTable Objects,
|
||||
MagicCatalog MagicCatalog,
|
||||
Spellbook Spellbook,
|
||||
ChatLog Chat,
|
||||
LocalPlayerState LocalPlayer,
|
||||
ItemManaState ItemMana,
|
||||
StackSplitQuantityState StackSplitQuantity,
|
||||
BufferedUiRegistry? UiRegistry,
|
||||
LiveCombatModeCommandSlot CombatModeCommands,
|
||||
ILocalPlayerIdentitySource PlayerIdentity,
|
||||
ILocalPlayerControllerSource PlayerController,
|
||||
ILocalPlayerModeSource PlayerMode,
|
||||
PlayerCharacterOptionsState CharacterOptions,
|
||||
ShortcutSnapshotState Shortcuts,
|
||||
Func<DeferredSelectionViewPlaneSource, SelectionCameraSource>
|
||||
SelectionCameraFactory,
|
||||
DeferredRenderFrameDiagnosticsSource FrameDiagnostics,
|
||||
VitalsVM? ExistingVitals,
|
||||
Action<string>? Toast,
|
||||
Func<double> ClientTime,
|
||||
Action<string> Log);
|
||||
|
||||
internal sealed record RetainedUiComposition(
|
||||
UiHost Host,
|
||||
RetailUiRuntime Runtime,
|
||||
VitalsVM Vitals,
|
||||
ChatVM Chat,
|
||||
CharacterSheetProvider CharacterSheet,
|
||||
MagicRuntime Magic,
|
||||
FrameScreenshotController? Screenshots);
|
||||
|
||||
internal sealed record InteractionRetainedUiResult(
|
||||
CombatAttackController CombatAttack,
|
||||
CombatTargetController CombatTarget,
|
||||
ExternalContainerLifecycleController ExternalContainerLifecycle,
|
||||
ItemInteractionController ItemInteraction,
|
||||
RetainedUiComposition? RetainedUi,
|
||||
InteractionUiLateBindings LateBindings);
|
||||
|
||||
internal interface IGameWindowInteractionRetainedUiPublication
|
||||
{
|
||||
void PublishInteractionRetainedUi(InteractionRetainedUiResult result);
|
||||
}
|
||||
|
||||
internal enum InteractionRetainedUiCompositionPoint
|
||||
{
|
||||
LateBindingsCreated,
|
||||
CombatAttackCreated,
|
||||
CombatTargetCreated,
|
||||
ExternalContainerLifecycleCreated,
|
||||
ItemInteractionCreated,
|
||||
RetainedUiDisabled,
|
||||
UiHostAcquired,
|
||||
InputCaptureBound,
|
||||
CursorAssetsCreated,
|
||||
CharacterSheetCreated,
|
||||
MagicRuntimeCreated,
|
||||
MouseInputWired,
|
||||
KeyboardInputWired,
|
||||
UiAssetsCreated,
|
||||
UiProbeCreated,
|
||||
UiRuntimeMounted,
|
||||
InventoryContainerBound,
|
||||
ResultPublished,
|
||||
}
|
||||
|
||||
internal sealed class InteractionUiLateBindings : IDisposable
|
||||
{
|
||||
private IDisposable? _inputCapture;
|
||||
private IDisposable? _inventoryContainer;
|
||||
private readonly List<(string Name, IDisposable Binding)> _lateOwnerBindings = [];
|
||||
private SelectionCameraSource? _selectionCamera;
|
||||
private bool _deactivationStarted;
|
||||
|
||||
public DeferredLiveSessionUiAuthority Session { get; } = new();
|
||||
public DeferredSelectionUiAuthority Selection { get; } = new();
|
||||
public DeferredSelectionViewPlaneSource SelectionViewPlane { get; } = new();
|
||||
public DeferredRadarSnapshotSource Radar { get; } = new();
|
||||
public DeferredInventoryContainerSource InventoryContainer { get; } = new();
|
||||
public DeferredWorldLifecycleAutomationRuntime Automation { get; } = new();
|
||||
public SelectionCameraSource SelectionCamera =>
|
||||
_selectionCamera ?? throw new InvalidOperationException(
|
||||
"The retained-UI selection camera is not initialized.");
|
||||
|
||||
public void InitializeSelectionCamera(SelectionCameraSource source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
||||
if (_selectionCamera is not null)
|
||||
throw new InvalidOperationException("The retained selection camera is already initialized.");
|
||||
_selectionCamera = source;
|
||||
}
|
||||
|
||||
public void AdoptInputCapture(IDisposable binding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(binding);
|
||||
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
||||
if (_inputCapture is not null)
|
||||
throw new InvalidOperationException("Retained input capture is already owned.");
|
||||
_inputCapture = binding;
|
||||
}
|
||||
|
||||
public void AdoptInventoryContainer(IDisposable binding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(binding);
|
||||
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
||||
if (_inventoryContainer is not null)
|
||||
throw new InvalidOperationException("Retained inventory binding is already owned.");
|
||||
_inventoryContainer = binding;
|
||||
}
|
||||
|
||||
public void AdoptLateOwnerBinding(string name, IDisposable binding)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
ArgumentNullException.ThrowIfNull(binding);
|
||||
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
||||
_lateOwnerBindings.Add((name, binding));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_deactivationStarted
|
||||
&& _lateOwnerBindings.Count == 0
|
||||
&& _inventoryContainer is null
|
||||
&& _inputCapture is null)
|
||||
return;
|
||||
_deactivationStarted = true;
|
||||
|
||||
List<Exception>? failures = null;
|
||||
Automation.Deactivate();
|
||||
Radar.Deactivate();
|
||||
SelectionViewPlane.Deactivate();
|
||||
Selection.Deactivate();
|
||||
Session.Deactivate();
|
||||
InventoryContainer.Deactivate();
|
||||
for (int i = _lateOwnerBindings.Count - 1; i >= 0; i--)
|
||||
{
|
||||
(string name, IDisposable binding) = _lateOwnerBindings[i];
|
||||
try
|
||||
{
|
||||
binding.Dispose();
|
||||
_lateOwnerBindings.RemoveAt(i);
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
(failures ??= []).Add(new InvalidOperationException(
|
||||
$"Retained UI late owner binding '{name}' did not detach.",
|
||||
failure));
|
||||
}
|
||||
}
|
||||
Release(ref _inventoryContainer, "inventory container binding", ref failures);
|
||||
Release(ref _inputCapture, "retained input capture", ref failures);
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException("Retained UI late binding cleanup failed.", failures);
|
||||
}
|
||||
|
||||
private static void Release(
|
||||
ref IDisposable? binding,
|
||||
string name,
|
||||
ref List<Exception>? failures)
|
||||
{
|
||||
IDisposable? current = binding;
|
||||
if (current is null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
current.Dispose();
|
||||
binding = null;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
(failures ??= []).Add(new InvalidOperationException(
|
||||
$"Retained UI {name} did not detach.",
|
||||
failure));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal interface IInteractionRetainedUiCompositionFactory
|
||||
{
|
||||
CombatAttackController CreateCombatAttack(
|
||||
InteractionRetainedUiDependencies dependencies);
|
||||
|
||||
CombatTargetController CreateCombatTarget(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
DeferredSelectionUiAuthority selection);
|
||||
|
||||
ExternalContainerLifecycleController CreateExternalContainerLifecycle(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
DeferredLiveSessionUiAuthority session);
|
||||
|
||||
ItemInteractionController CreateItemInteraction(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
InteractionUiLateBindings lateBindings);
|
||||
|
||||
RetainedUiComposition CreateRetainedUi(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
InteractionUiLateBindings lateBindings,
|
||||
RetailUiRuntimeLease lease,
|
||||
CombatAttackController combatAttack,
|
||||
ItemInteractionController itemInteraction,
|
||||
Action<InteractionRetainedUiCompositionPoint> checkpoint);
|
||||
|
||||
void Release(IDisposable resource);
|
||||
}
|
||||
|
||||
internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||
: IInteractionRetainedUiCompositionFactory
|
||||
{
|
||||
public CombatAttackController CreateCombatAttack(
|
||||
InteractionRetainedUiDependencies d) =>
|
||||
new(d.Combat, d.CombatAttackOperations);
|
||||
|
||||
public CombatTargetController CreateCombatTarget(
|
||||
InteractionRetainedUiDependencies d,
|
||||
DeferredSelectionUiAuthority selection) =>
|
||||
new(
|
||||
d.Combat,
|
||||
d.Selection,
|
||||
autoTarget: () => d.Settings.Gameplay.AutoTarget,
|
||||
selectClosestTarget: () =>
|
||||
selection.SelectClosestCombatTarget(showToast: false));
|
||||
|
||||
public ExternalContainerLifecycleController CreateExternalContainerLifecycle(
|
||||
InteractionRetainedUiDependencies d,
|
||||
DeferredLiveSessionUiAuthority session) =>
|
||||
new(
|
||||
d.ExternalContainers,
|
||||
d.Objects,
|
||||
guid => session.CurrentSession?.SendNoLongerViewingContents(guid));
|
||||
|
||||
public ItemInteractionController CreateItemInteraction(
|
||||
InteractionRetainedUiDependencies d,
|
||||
InteractionUiLateBindings late)
|
||||
{
|
||||
DeferredLiveSessionUiAuthority session = late.Session;
|
||||
DeferredSelectionUiAuthority selection = late.Selection;
|
||||
return new ItemInteractionController(
|
||||
d.Objects,
|
||||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||
sendUse: selection.SendUse,
|
||||
sendExamine: guid => session.CurrentSession?.SendAppraise(guid),
|
||||
sendUseWithTarget: (source, target) =>
|
||||
session.CurrentSession?.SendUseWithTarget(source, target),
|
||||
sendWield: (item, mask) =>
|
||||
session.CurrentSession?.SendGetAndWieldItem(item, mask),
|
||||
sendDrop: item => session.CurrentSession?.SendDropItem(item),
|
||||
sendGive: (target, item, amount) =>
|
||||
session.CurrentSession?.SendGiveObject(target, item, amount),
|
||||
dragOnPlayerOpensSecureTrade: () =>
|
||||
d.CharacterOptions.DragItemOnPlayerOpensSecureTrade,
|
||||
toast: d.Toast,
|
||||
readyForInventoryRequest: () => session.IsInWorld,
|
||||
playerOnGround: () =>
|
||||
d.PlayerMode.IsPlayerMode
|
||||
&& d.PlayerController.Controller is { IsAirborne: false },
|
||||
inNonCombatMode: () =>
|
||||
d.Combat.CurrentMode == CombatMode.NonCombat,
|
||||
combatState: d.Combat,
|
||||
sendChangeCombatMode: mode =>
|
||||
session.CurrentSession?.SendChangeCombatMode(mode),
|
||||
isComponentPack: d.MagicCatalog.IsComponentPack,
|
||||
placeInBackpack: selection.SendPickup,
|
||||
backpackContainerId: () => late.InventoryContainer.Current(
|
||||
d.PlayerIdentity.ServerGuid),
|
||||
groundObjectId: () => d.ExternalContainers.CurrentContainerId,
|
||||
sendSplitToWorld: (item, amount) =>
|
||||
session.CurrentSession?.SendStackableSplitTo3D(item, amount),
|
||||
selectedObjectId: () => d.Selection.SelectedObjectId ?? 0u,
|
||||
stackSplitQuantity: d.StackSplitQuantity,
|
||||
systemMessage: text => d.Chat.OnSystemMessage(text, 0x1Au),
|
||||
sendPutItemInContainer: (item, container, placement) =>
|
||||
session.CurrentSession?.SendPutItemInContainer(
|
||||
item,
|
||||
container,
|
||||
placement),
|
||||
sendSplitToContainer: (item, container, placement, amount) =>
|
||||
session.CurrentSession?.SendStackableSplitToContainer(
|
||||
item,
|
||||
container,
|
||||
placement,
|
||||
amount),
|
||||
requestExternalContainer: guid =>
|
||||
{
|
||||
d.ExternalContainers.RequestOpen(guid);
|
||||
});
|
||||
}
|
||||
|
||||
public RetainedUiComposition CreateRetainedUi(
|
||||
InteractionRetainedUiDependencies d,
|
||||
InteractionUiLateBindings late,
|
||||
RetailUiRuntimeLease lease,
|
||||
CombatAttackController combatAttack,
|
||||
ItemInteractionController itemInteraction,
|
||||
Action<InteractionRetainedUiCompositionPoint> checkpoint)
|
||||
{
|
||||
IDisposable? inputCapture = null;
|
||||
IDisposable? inventoryContainer = null;
|
||||
try
|
||||
{
|
||||
VitalsVM vitals = d.ExistingVitals
|
||||
?? new VitalsVM(d.Combat, d.LocalPlayer);
|
||||
UiHost host = lease.AcquireHost(
|
||||
() => new UiHost(
|
||||
d.Gl,
|
||||
d.ShadersDirectory,
|
||||
d.DebugFont,
|
||||
d.HostQuiescence));
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.UiHostAcquired);
|
||||
inputCapture = d.RetainedInputCapture.Bind(host.Root);
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.InputCaptureBound);
|
||||
host.Root.UiLocked = d.Settings.Gameplay.LockUI;
|
||||
|
||||
var cursorFeedback = new CursorFeedbackController(
|
||||
itemInteraction,
|
||||
worldTargetProvider: () =>
|
||||
late.Selection.PickAtCursor(includeSelf: true) ?? 0u,
|
||||
combatModeProvider: () => d.Combat.CurrentMode);
|
||||
var cursorManager = new RetailCursorManager(d.Dats, d.DatLock);
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.CursorAssetsCreated);
|
||||
|
||||
var characterSheet = new CharacterSheetProvider(
|
||||
d.Objects,
|
||||
d.LocalPlayer,
|
||||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||
activeToonName: () => d.Settings.ActiveToonKey,
|
||||
fallbackSheet: Studio.SampleData.SampleCharacter,
|
||||
canSendRaise: () => late.Session.IsInWorld,
|
||||
sendRaiseAttribute: (statId, cost) =>
|
||||
late.Session.CurrentSession?.SendRaiseAttribute(statId, cost),
|
||||
sendRaiseVital: (statId, cost) =>
|
||||
late.Session.CurrentSession?.SendRaiseVital(statId, cost),
|
||||
sendRaiseSkill: (statId, cost) =>
|
||||
late.Session.CurrentSession?.SendRaiseSkill(statId, cost),
|
||||
sendTrainSkill: (statId, credits) =>
|
||||
late.Session.CurrentSession?.SendTrainSkill(statId, credits));
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.CharacterSheetCreated);
|
||||
|
||||
MagicRuntime magic = MagicRuntime.Create(
|
||||
d.MagicCatalog,
|
||||
d.Spellbook,
|
||||
d.Objects,
|
||||
selectedObject: () => d.Selection.SelectedObjectId,
|
||||
localPlayerId: () => d.PlayerIdentity.ServerGuid,
|
||||
accountName: () => late.Session.AccountName
|
||||
?? d.Options.LiveUser
|
||||
?? string.Empty,
|
||||
stopCompletely: d.CombatAttackOperations.PrepareAttackRequest,
|
||||
sendUntargeted: spellId =>
|
||||
late.Session.CurrentSession?.SendCastUntargetedSpell(spellId),
|
||||
sendTargeted: (target, spellId) =>
|
||||
late.Session.CurrentSession?.SendCastTargetedSpell(target, spellId),
|
||||
displayMessage: text => d.Chat.OnSystemMessage(text, 0x1Au),
|
||||
incrementBusy: itemInteraction.IncrementBusyCount,
|
||||
canSend: () => late.Session.IsInWorld);
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.MagicRuntimeCreated);
|
||||
|
||||
foreach (IMouse mouse in d.Input.Mice)
|
||||
host.WireMouse(mouse);
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.MouseInputWired);
|
||||
foreach (IKeyboard keyboard in d.Input.Keyboards)
|
||||
host.WireKeyboard(keyboard);
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.KeyboardInputWired);
|
||||
|
||||
(uint, int, int) ResolveChrome(uint id)
|
||||
{
|
||||
uint texture = d.TextureCache.GetOrUploadRenderSurface(
|
||||
id,
|
||||
out int width,
|
||||
out int height);
|
||||
return (texture, width, height);
|
||||
}
|
||||
|
||||
var iconComposer = new IconComposer(d.Dats, d.TextureCache);
|
||||
ControlsIni controls = d.Options.AcDir is { } acDir
|
||||
? ControlsIni.Load(Path.Combine(acDir, "controls", "controls.ini"))
|
||||
: ControlsIni.Parse(string.Empty);
|
||||
UiDatFont? defaultFont;
|
||||
lock (d.DatLock)
|
||||
defaultFont = UiDatFont.Load(d.Dats, d.TextureCache);
|
||||
var datFontCache = new ConcurrentDictionary<uint, UiDatFont?>();
|
||||
if (defaultFont is not null)
|
||||
datFontCache.TryAdd(UiDatFont.DefaultFontId, defaultFont);
|
||||
UiDatFont? ResolveDatFont(uint fontDid) =>
|
||||
datFontCache.GetOrAdd(fontDid, id =>
|
||||
{
|
||||
lock (d.DatLock)
|
||||
return UiDatFont.Load(d.Dats, d.TextureCache, id);
|
||||
});
|
||||
d.Log(defaultFont is not null
|
||||
? "[D.2b] vitals dat-font 0x40000000 loaded for numeric overlay."
|
||||
: "[D.2b] vitals dat-font 0x40000000 unavailable — falling back to debug font.");
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.UiAssetsCreated);
|
||||
|
||||
host.Root.Width = d.Window.Size.X;
|
||||
host.Root.Height = d.Window.Size.Y;
|
||||
var chat = new ChatVM(d.Chat, displayLimit: 200);
|
||||
AcDream.UI.Abstractions.Panels.Settings.SettingsStore? layoutStore =
|
||||
d.Settings.LayoutStore;
|
||||
RetailUiPersistenceBindings? persistence = layoutStore is null
|
||||
? null
|
||||
: new RetailUiPersistenceBindings(
|
||||
layoutStore,
|
||||
CharacterKey: () => d.Settings.ActiveToonKey,
|
||||
ScreenSize: () => (d.Window.Size.X, d.Window.Size.Y));
|
||||
void ProbeLog(string message) => d.Log("[UI-PROBE] " + message);
|
||||
FrameScreenshotController? screenshots = null;
|
||||
if (d.Options.UiProbeEnabled
|
||||
&& d.Options.AutomationArtifactDirectory is { } artifactDirectory)
|
||||
{
|
||||
screenshots = new FrameScreenshotController(
|
||||
d.Gl,
|
||||
Path.Combine(artifactDirectory, "screenshots"),
|
||||
ProbeLog);
|
||||
}
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.UiProbeCreated);
|
||||
|
||||
var assets = new RetailUiAssets(
|
||||
d.Dats,
|
||||
d.DatLock,
|
||||
ResolveChrome,
|
||||
ResolveDatFont,
|
||||
defaultFont,
|
||||
d.DebugFont,
|
||||
controls,
|
||||
iconComposer);
|
||||
var bindings = new RetailUiRuntimeBindings(
|
||||
Host: host,
|
||||
Assets: assets,
|
||||
Vitals: new VitalsRuntimeBindings(vitals),
|
||||
Chat: new ChatRuntimeBindings(chat, () => late.Session.Commands),
|
||||
Radar: new RadarRuntimeBindings(
|
||||
late.Radar.Snapshot,
|
||||
d.Selection,
|
||||
d.Settings.SetUiLocked),
|
||||
Combat: new CombatRuntimeBindings(
|
||||
d.Combat,
|
||||
combatAttack,
|
||||
() => d.Settings.Gameplay,
|
||||
d.Settings.SetCombatGameplay),
|
||||
Magic: new MagicRuntimeBindings(
|
||||
d.Spellbook,
|
||||
magic.Casting,
|
||||
d.Objects,
|
||||
() => d.PlayerIdentity.ServerGuid,
|
||||
d.MagicCatalog.Components,
|
||||
iconComposer.GetIcon,
|
||||
iconComposer.GetDragIcon,
|
||||
iconComposer.GetSpellIcon,
|
||||
iconComposer.GetSpellComponentIcon,
|
||||
d.Selection,
|
||||
d.MagicCatalog.GetSpellLevel,
|
||||
guid => d.Selection.Select(guid, SelectionChangeSource.Inventory),
|
||||
guid => late.Session.TryUseItem(guid, d.Log),
|
||||
(tab, position, spellId) =>
|
||||
late.Session.CurrentSession?.SendAddSpellFavorite(
|
||||
spellId,
|
||||
position,
|
||||
tab),
|
||||
(tab, spellId) =>
|
||||
late.Session.CurrentSession?.SendRemoveSpellFavorite(spellId, tab),
|
||||
filters => late.Session.CurrentSession?.SendSpellbookFilter(filters),
|
||||
spellId => late.Session.CurrentSession?.SendRemoveSpell(spellId),
|
||||
(componentId, amount) =>
|
||||
late.Session.CurrentSession?.SendSetDesiredComponentLevel(
|
||||
componentId,
|
||||
amount),
|
||||
d.ClientTime),
|
||||
JumpPowerbar: new JumpPowerbarRuntimeBindings(
|
||||
() => d.PlayerController.Controller?.JumpCharge ?? default),
|
||||
Fps: new FpsRuntimeBindings(
|
||||
() => d.FrameDiagnostics.Snapshot.Fps,
|
||||
() => 1.0,
|
||||
() => d.Settings.DisplayPreview.ShowFps),
|
||||
VividTarget: new VividTargetRuntimeBindings(
|
||||
d.Selection,
|
||||
() => d.PlayerIdentity.ServerGuid,
|
||||
() => d.Settings.Gameplay.VividTargetingIndicator,
|
||||
late.Selection.ResolveVividTargetInfo,
|
||||
late.SelectionCamera.UiSnapshot),
|
||||
Indicators: new IndicatorRuntimeBindings(
|
||||
d.Spellbook,
|
||||
d.Objects,
|
||||
() => d.PlayerIdentity.ServerGuid,
|
||||
() => d.LocalPlayer.GetAttribute(
|
||||
LocalPlayerState.AttributeKind.Strength)
|
||||
is { } strength ? (int?)strength.Current : null,
|
||||
() => late.Session.LinkStatus,
|
||||
d.ClientTime,
|
||||
() => late.Session.CurrentSession?.RequestLinkStatusPing(),
|
||||
d.Window.Close),
|
||||
Toolbar: new ToolbarRuntimeBindings(
|
||||
d.Objects,
|
||||
() => d.Shortcuts.Items,
|
||||
iconComposer.GetIcon,
|
||||
iconComposer.GetDragIcon,
|
||||
guid => late.Session.TryUseItem(guid, d.Log),
|
||||
d.Combat,
|
||||
d.ItemMana,
|
||||
d.CombatModeCommands.Toggle,
|
||||
itemInteraction,
|
||||
entry => late.Session.CurrentSession?.SendAddShortcut(entry),
|
||||
index => late.Session.CurrentSession?.SendRemoveShortcut(index),
|
||||
d.Selection,
|
||||
handler => d.Combat.HealthChanged += handler,
|
||||
handler => d.Combat.HealthChanged -= handler,
|
||||
late.Selection.ShouldShowHealth,
|
||||
guid => d.Objects.Get(guid)?.GetAppropriateName(),
|
||||
d.Combat.GetHealthPercent,
|
||||
d.Combat.HasHealth,
|
||||
guid => (uint)(d.Objects.Get(guid)?.StackSize ?? 0),
|
||||
guid => late.Session.CurrentSession?.SendQueryHealth(guid),
|
||||
guid => late.Session.CurrentSession?.SendQueryItemMana(guid),
|
||||
() => d.PlayerIdentity.ServerGuid,
|
||||
(item, container, placement) =>
|
||||
late.Session.CurrentSession?.SendPutItemInContainer(
|
||||
item,
|
||||
container,
|
||||
placement)),
|
||||
Character: new CharacterRuntimeBindings(characterSheet),
|
||||
Inventory: new InventoryRuntimeBindings(
|
||||
d.Objects,
|
||||
() => d.PlayerIdentity.ServerGuid,
|
||||
iconComposer.GetIcon,
|
||||
iconComposer.GetDragIcon,
|
||||
() => d.LocalPlayer.GetAttribute(
|
||||
LocalPlayerState.AttributeKind.Strength)
|
||||
is { } strength ? (int?)strength.Current : null,
|
||||
guid => late.Session.CurrentSession?.SendUse(guid),
|
||||
(item, container, placement) =>
|
||||
late.Session.CurrentSession?.SendPutItemInContainer(
|
||||
item,
|
||||
container,
|
||||
placement),
|
||||
(item, container, placement, amount) =>
|
||||
late.Session.CurrentSession?.SendStackableSplitToContainer(
|
||||
item,
|
||||
container,
|
||||
placement,
|
||||
amount),
|
||||
(source, target, amount) =>
|
||||
late.Session.CurrentSession?.SendStackableMerge(
|
||||
source,
|
||||
target,
|
||||
amount),
|
||||
itemInteraction,
|
||||
d.Selection),
|
||||
ExternalContainer: new ExternalContainerRuntimeBindings(
|
||||
d.ExternalContainers,
|
||||
d.Objects,
|
||||
iconComposer.GetIcon,
|
||||
iconComposer.GetDragIcon,
|
||||
itemInteraction,
|
||||
d.Selection,
|
||||
guid => late.Session.CurrentSession?.SendUse(guid),
|
||||
(item, container, placement) =>
|
||||
late.Session.CurrentSession?.SendPutItemInContainer(
|
||||
item,
|
||||
container,
|
||||
placement),
|
||||
(item, container, placement, amount) =>
|
||||
late.Session.CurrentSession?.SendStackableSplitToContainer(
|
||||
item,
|
||||
container,
|
||||
placement,
|
||||
amount),
|
||||
late.Selection.IsWithinExternalContainerUseRange),
|
||||
Cursor: new RetailUiCursorBindings(cursorFeedback, cursorManager),
|
||||
Confirmations: new ConfirmationRuntimeBindings(
|
||||
(type, context, accepted) =>
|
||||
late.Session.CurrentSession?.SendConfirmationResponse(
|
||||
type,
|
||||
context,
|
||||
accepted)),
|
||||
StackSplitQuantity: d.StackSplitQuantity,
|
||||
Plugins: d.UiRegistry,
|
||||
Persistence: persistence,
|
||||
Probe: new RetailUiProbeBindings(
|
||||
d.Options.UiProbeEnabled,
|
||||
d.Options.UiProbeScript,
|
||||
d.Options.UiProbeDump,
|
||||
ProbeLog,
|
||||
action => d.InputDispatcher?.TryInvokeAutomationAction(action) == true,
|
||||
(action, held) =>
|
||||
d.InputDispatcher?.TrySetAutomationActionHeld(action, held) == true,
|
||||
late.Automation));
|
||||
RetailUiRuntime runtime = lease.Mount(
|
||||
() => RetailUiRuntime.CreateUninitialized(bindings));
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.UiRuntimeMounted);
|
||||
inventoryContainer = late.InventoryContainer.Bind(runtime);
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.InventoryContainerBound);
|
||||
|
||||
late.AdoptInputCapture(inputCapture);
|
||||
inputCapture = null;
|
||||
late.AdoptInventoryContainer(inventoryContainer);
|
||||
inventoryContainer = null;
|
||||
return new RetainedUiComposition(
|
||||
host,
|
||||
runtime,
|
||||
vitals,
|
||||
chat,
|
||||
characterSheet,
|
||||
magic,
|
||||
screenshots);
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
List<Exception>? cleanup = null;
|
||||
TryRelease(ref inventoryContainer, "inventory container", ref cleanup);
|
||||
TryRelease(ref inputCapture, "input capture", ref cleanup);
|
||||
if (cleanup is not null)
|
||||
{
|
||||
cleanup.Insert(0, failure);
|
||||
throw new AggregateException(
|
||||
"Retained UI construction and local binding rollback failed.",
|
||||
cleanup);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Release(IDisposable resource) => resource.Dispose();
|
||||
|
||||
private static void TryRelease(
|
||||
ref IDisposable? resource,
|
||||
string name,
|
||||
ref List<Exception>? failures)
|
||||
{
|
||||
if (resource is null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
resource.Dispose();
|
||||
resource = null;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
(failures ??= []).Add(new InvalidOperationException(
|
||||
$"Retained UI {name} rollback failed.",
|
||||
failure));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class InteractionRetainedUiCompositionPhase
|
||||
: IInteractionUiCompositionPhase<
|
||||
HostInputCameraResult,
|
||||
ContentEffectsAudioResult,
|
||||
SettingsDevToolsResult,
|
||||
WorldRenderResult,
|
||||
InteractionRetainedUiResult>
|
||||
{
|
||||
private readonly InteractionRetainedUiDependencies _dependencies;
|
||||
private readonly RetailUiRuntimeLease _retainedUiLease;
|
||||
private readonly IGameWindowInteractionRetainedUiPublication _publication;
|
||||
private readonly IInteractionRetainedUiCompositionFactory _factory;
|
||||
private readonly Action<InteractionRetainedUiCompositionPoint>? _faultInjection;
|
||||
|
||||
public InteractionRetainedUiCompositionPhase(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
RetailUiRuntimeLease retainedUiLease,
|
||||
IGameWindowInteractionRetainedUiPublication publication,
|
||||
IInteractionRetainedUiCompositionFactory? factory = null,
|
||||
Action<InteractionRetainedUiCompositionPoint>? faultInjection = null)
|
||||
{
|
||||
_dependencies = dependencies
|
||||
?? throw new ArgumentNullException(nameof(dependencies));
|
||||
_retainedUiLease = retainedUiLease
|
||||
?? throw new ArgumentNullException(nameof(retainedUiLease));
|
||||
_publication = publication
|
||||
?? throw new ArgumentNullException(nameof(publication));
|
||||
_factory = factory ?? new RetailInteractionRetainedUiCompositionFactory();
|
||||
_faultInjection = faultInjection;
|
||||
}
|
||||
|
||||
public InteractionRetainedUiResult Compose()
|
||||
{
|
||||
var scope = new CompositionAcquisitionScope();
|
||||
try
|
||||
{
|
||||
var lateLease = scope.Acquire(
|
||||
"retained UI late bindings",
|
||||
static () => new InteractionUiLateBindings(),
|
||||
_factory.Release);
|
||||
InteractionUiLateBindings late = lateLease.Resource;
|
||||
late.InitializeSelectionCamera(
|
||||
_dependencies.SelectionCameraFactory(late.SelectionViewPlane));
|
||||
Fault(InteractionRetainedUiCompositionPoint.LateBindingsCreated);
|
||||
|
||||
var attackLease = scope.Acquire(
|
||||
"combat attack controller",
|
||||
() => _factory.CreateCombatAttack(_dependencies),
|
||||
_factory.Release);
|
||||
Fault(InteractionRetainedUiCompositionPoint.CombatAttackCreated);
|
||||
var targetLease = scope.Acquire(
|
||||
"combat target controller",
|
||||
() => _factory.CreateCombatTarget(_dependencies, late.Selection),
|
||||
_factory.Release);
|
||||
Fault(InteractionRetainedUiCompositionPoint.CombatTargetCreated);
|
||||
var externalLease = scope.Acquire(
|
||||
"external container lifecycle",
|
||||
() => _factory.CreateExternalContainerLifecycle(
|
||||
_dependencies,
|
||||
late.Session),
|
||||
_factory.Release);
|
||||
Fault(InteractionRetainedUiCompositionPoint.ExternalContainerLifecycleCreated);
|
||||
var itemLease = scope.Acquire(
|
||||
"item interaction controller",
|
||||
() => _factory.CreateItemInteraction(_dependencies, late),
|
||||
_factory.Release);
|
||||
Fault(InteractionRetainedUiCompositionPoint.ItemInteractionCreated);
|
||||
|
||||
var uiLease = scope.Own(
|
||||
"retained UI runtime lease",
|
||||
_retainedUiLease,
|
||||
_factory.Release);
|
||||
RetainedUiComposition? retainedUi = null;
|
||||
if (_dependencies.Options.RetailUi)
|
||||
{
|
||||
retainedUi = _factory.CreateRetainedUi(
|
||||
_dependencies,
|
||||
late,
|
||||
_retainedUiLease,
|
||||
attackLease.Resource,
|
||||
itemLease.Resource,
|
||||
Fault);
|
||||
}
|
||||
else
|
||||
{
|
||||
Fault(InteractionRetainedUiCompositionPoint.RetainedUiDisabled);
|
||||
}
|
||||
|
||||
var result = new InteractionRetainedUiResult(
|
||||
attackLease.Resource,
|
||||
targetLease.Resource,
|
||||
externalLease.Resource,
|
||||
itemLease.Resource,
|
||||
retainedUi,
|
||||
late);
|
||||
_publication.PublishInteractionRetainedUi(result);
|
||||
lateLease.Transfer();
|
||||
attackLease.Transfer();
|
||||
targetLease.Transfer();
|
||||
externalLease.Transfer();
|
||||
itemLease.Transfer();
|
||||
uiLease.Transfer();
|
||||
Fault(InteractionRetainedUiCompositionPoint.ResultPublished);
|
||||
scope.Complete();
|
||||
return result;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
scope.RollbackAndThrow(failure);
|
||||
throw new System.Diagnostics.UnreachableException();
|
||||
}
|
||||
}
|
||||
|
||||
public InteractionRetainedUiResult Compose(
|
||||
HostInputCameraResult host,
|
||||
ContentEffectsAudioResult content,
|
||||
SettingsDevToolsResult settings,
|
||||
WorldRenderResult world)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(host);
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
ArgumentNullException.ThrowIfNull(world);
|
||||
if (!ReferenceEquals(_dependencies.InputDispatcher, host.InputDispatcher)
|
||||
|| !ReferenceEquals(_dependencies.Dats, content.Dats)
|
||||
|| !ReferenceEquals(_dependencies.MagicCatalog, content.MagicCatalog)
|
||||
|| !ReferenceEquals(_dependencies.TextureCache, world.Foundation.TextureCache)
|
||||
|| !ReferenceEquals(_dependencies.DebugFont, world.Foundation.DebugFont))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Interaction/UI dependencies do not match the ordered phase results.");
|
||||
}
|
||||
|
||||
return Compose();
|
||||
}
|
||||
|
||||
private void Fault(InteractionRetainedUiCompositionPoint point) =>
|
||||
_faultInjection?.Invoke(point);
|
||||
}
|
||||
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