Move the retail one-request-at-a-time gate, shared use busy references, external-container state, item mana, shortcuts, and desired-component snapshots into one Runtime-owned graph over J3's exact ClientObjectTable. Retained UI and session routing now borrow that owner; reset and shutdown preserve the existing order while failure/reentrancy tests protect the transaction boundary. Co-authored-by: Codex <codex@openai.com>
887 lines
37 KiB
C#
887 lines
37 KiB
C#
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.Runtime.Gameplay;
|
|
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,
|
|
RuntimeInventoryState Inventory,
|
|
MagicCatalog MagicCatalog,
|
|
Spellbook Spellbook,
|
|
RuntimeCommunicationState Communication,
|
|
LocalPlayerState LocalPlayer,
|
|
StackSplitQuantityState StackSplitQuantity,
|
|
BufferedUiRegistry? UiRegistry,
|
|
LiveCombatModeCommandSlot CombatModeCommands,
|
|
ILocalPlayerIdentitySource PlayerIdentity,
|
|
ILocalPlayerControllerSource PlayerController,
|
|
ILocalPlayerModeSource PlayerMode,
|
|
PlayerCharacterOptionsState CharacterOptions,
|
|
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.Inventory.ExternalContainers,
|
|
d.Inventory.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.Inventory.Objects,
|
|
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
|
sendUse: null,
|
|
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.Inventory.ExternalContainers.CurrentContainerId,
|
|
sendSplitToWorld: (item, amount) =>
|
|
session.CurrentSession?.SendStackableSplitTo3D(item, amount),
|
|
selectedObjectId: () => d.Selection.SelectedObjectId ?? 0u,
|
|
stackSplitQuantity: d.StackSplitQuantity,
|
|
systemMessage:
|
|
text => d.Communication.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.Inventory.ExternalContainers.RequestOpen(guid);
|
|
},
|
|
requestUse: selection.RequestUse,
|
|
transactions: d.Inventory.Transactions);
|
|
}
|
|
|
|
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.Inventory.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.Inventory.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.Communication.Chat.OnSystemMessage(text, 0x1Au),
|
|
incrementBusy: itemInteraction.IncrementBusyCount,
|
|
canSend: () => late.Session.IsInWorld);
|
|
checkpoint(InteractionRetainedUiCompositionPoint.MagicRuntimeCreated);
|
|
|
|
uint MagicSkillLevel(MagicSchool school)
|
|
{
|
|
static uint SkillId(MagicSchool value) => value switch
|
|
{
|
|
MagicSchool.CreatureEnchantment => 0x1Fu,
|
|
MagicSchool.ItemEnchantment => 0x20u,
|
|
MagicSchool.LifeMagic => 0x21u,
|
|
MagicSchool.WarMagic => 0x22u,
|
|
MagicSchool.VoidMagic => 0x2Bu,
|
|
_ => 0u,
|
|
};
|
|
|
|
uint skillId = SkillId(school);
|
|
if (skillId != 0u)
|
|
return d.LocalPlayer.GetSkill(skillId)?.CurrentLevel ?? 0u;
|
|
|
|
uint highest = 0u;
|
|
foreach (MagicSchool candidate in new[]
|
|
{
|
|
MagicSchool.CreatureEnchantment,
|
|
MagicSchool.ItemEnchantment,
|
|
MagicSchool.LifeMagic,
|
|
MagicSchool.WarMagic,
|
|
MagicSchool.VoidMagic,
|
|
})
|
|
{
|
|
highest = Math.Max(
|
|
highest,
|
|
d.LocalPlayer.GetSkill(SkillId(candidate))?.CurrentLevel ?? 0u);
|
|
}
|
|
return highest;
|
|
}
|
|
|
|
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.Communication.Chat,
|
|
displayLimit: 200,
|
|
commandTargets: d.Communication.CommandTargets);
|
|
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.Inventory.Objects,
|
|
() => d.PlayerIdentity.ServerGuid,
|
|
d.MagicCatalog.Components,
|
|
iconComposer.GetIcon,
|
|
iconComposer.GetDragIcon,
|
|
iconComposer.GetSpellIcon,
|
|
iconComposer.GetSpellComponentIcon,
|
|
d.Selection,
|
|
d.MagicCatalog.GetSpellLevel,
|
|
magic.GetExamineComponents,
|
|
MagicSkillLevel,
|
|
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.Inventory.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.Inventory.Objects,
|
|
() => d.Inventory.Shortcuts.Items,
|
|
iconComposer.GetIcon,
|
|
iconComposer.GetDragIcon,
|
|
guid => late.Session.TryUseItem(guid, d.Log),
|
|
d.Combat,
|
|
d.Inventory.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.Inventory.Objects.Get(guid)?.GetAppropriateName(),
|
|
d.Combat.GetHealthPercent,
|
|
d.Combat.HasHealth,
|
|
guid =>
|
|
(uint)(d.Inventory.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.Inventory.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.Inventory.ExternalContainers,
|
|
d.Inventory.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)),
|
|
Appraisal: new AppraisalRuntimeBindings(
|
|
characterSheet.CharacterName,
|
|
(item, inscription) =>
|
|
late.Session.CurrentSession?.SendSetInscription(
|
|
item,
|
|
inscription),
|
|
text =>
|
|
d.Communication.Chat.OnSystemMessage(text, 0x1Au)),
|
|
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<
|
|
GameWindowPlatformResult<GL, IInputContext>,
|
|
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(
|
|
GameWindowPlatformResult<GL, IInputContext> platform,
|
|
HostInputCameraResult host,
|
|
ContentEffectsAudioResult content,
|
|
SettingsDevToolsResult settings,
|
|
WorldRenderResult world)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(platform);
|
|
ArgumentNullException.ThrowIfNull(host);
|
|
ArgumentNullException.ThrowIfNull(content);
|
|
ArgumentNullException.ThrowIfNull(settings);
|
|
ArgumentNullException.ThrowIfNull(world);
|
|
if (!ReferenceEquals(_dependencies.Gl, platform.Graphics)
|
|
|| !ReferenceEquals(_dependencies.Input, platform.Input)
|
|
|| !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);
|
|
}
|