Fills TS-82's Summary placeholder with a faithful port of gmCGSummaryPage (name field with NameInputFilter + the retail commit-on-focus-lost/submit dispatch + the >32-char ID_CharGen_NameTooLong reject-and-revert path, the REAL three-row-template listbox confirmed against the installed EoR dat before writing any page code, and Summary's own independent gmCG3DView preview instance wired through a second ChargenPreviewController pair mirroring the Appearance page's exact composition shape). Ports CharGenState::RandomizeCharacter and its six sub-primitives into RuntimeCharacterCreationState — not approximated: the RandInt/RollDice semantics are independently confirmed from both the decompiled RNG bodies and the CharGenStateVtbl union struct in acclient.h. Three consumers: the chargen screen's open-roll (retiring AP-214's honest-blank deviation and reproducing the Appearance page's gender-flip-on-init quirk), the Summary page's Random button (behind the retail randomize-warning confirm), and the Appearance page's Random button (narrowing AP-212 to just Heritage/Profession/Town's still-approximated rolls and Skills' still-unported RandomizeSkills). Wires the Finish button (previously ghosted) with retail's NoName/ CreditWarning dialog pair, adds the F12 amendment's HeritageOrGenderUnset local refusal to TryBeginFinish (register AP-223) as a defensive backstop now that the screen-open roll normally makes it unreachable, and wires the four ID_Character_Err_* rejection dialogs for the 0xF643 response codes CC3 already parsed but nothing displayed. Register: TS-82 retired, AP-214 retired, AP-212 narrowed, AP-223/224/225 filed (heritage/gender Finish refusal, Summary's two-bucket skill-list narrowing, the 32-vs-33 name-length threshold reconciliation). Runtime 1722/0 (was 1713), App 5240/3 skips (was 5223/3), Headless 166/0 unchanged, full solution Release build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
861 lines
29 KiB
C#
861 lines
29 KiB
C#
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.Items;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Session;
|
|
using AcDream.UI.Abstractions;
|
|
using Silk.NET.Windowing;
|
|
|
|
namespace AcDream.App.Composition;
|
|
|
|
/// <summary>
|
|
/// Early retained-UI projection over Slice J's later current-runtime seam.
|
|
/// Every call captures the view and command owner together and supplies that
|
|
/// exact generation, so a displaced session can never receive the command.
|
|
/// </summary>
|
|
internal sealed class DeferredGameRuntimeStateCommands
|
|
{
|
|
private readonly object _gate = new();
|
|
private IGameRuntimeView? _view;
|
|
private IGameRuntimeCommands? _commands;
|
|
private bool _deactivated;
|
|
|
|
public bool IsInWorld
|
|
{
|
|
get
|
|
{
|
|
lock (_gate)
|
|
return !_deactivated
|
|
&& _view?.Lifecycle.State == RuntimeLifecycleState.InWorld;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Borrows the current adapter's character-selection projection. The
|
|
/// reference is deliberately not cached here: releasing or displacing the
|
|
/// exact late binding makes the next read return <see langword="null"/>,
|
|
/// while <c>CurrentGameRuntimeAdapter</c> keeps an already-borrowed reference
|
|
/// inert if disposal races the render-thread consumer.
|
|
/// </summary>
|
|
public IRuntimeCharacterSelectionView? CharacterSelection
|
|
{
|
|
get
|
|
{
|
|
lock (_gate)
|
|
return !_deactivated && _view is not null
|
|
? _view.CharacterSelection
|
|
: null;
|
|
}
|
|
}
|
|
|
|
/// <summary>Campaign CC slice CC4: same late-bound borrow shape as
|
|
/// <see cref="CharacterSelection"/>.</summary>
|
|
public IRuntimeCharacterCreationView? CharacterCreation
|
|
{
|
|
get
|
|
{
|
|
lock (_gate)
|
|
return !_deactivated && _view is not null
|
|
? _view.CharacterCreation
|
|
: null;
|
|
}
|
|
}
|
|
|
|
public IDisposable Bind(
|
|
IGameRuntimeView view,
|
|
IGameRuntimeCommands commands)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(view);
|
|
ArgumentNullException.ThrowIfNull(commands);
|
|
lock (_gate)
|
|
{
|
|
ObjectDisposedException.ThrowIf(_deactivated, this);
|
|
if (_view is not null || _commands is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The retained-UI game-runtime command seam is already bound.");
|
|
}
|
|
_view = view;
|
|
_commands = commands;
|
|
}
|
|
return new ExpectedRuntimeBinding(this, view, commands);
|
|
}
|
|
|
|
public RuntimeCommandResult AddShortcut(ShortcutEntry entry) =>
|
|
Invoke((commands, generation) => commands.InventoryState.AddShortcut(
|
|
generation,
|
|
new RuntimeShortcutCommand(
|
|
entry.Index,
|
|
entry.ObjectId,
|
|
entry.SpellId)));
|
|
|
|
public RuntimeCommandResult RemoveShortcut(uint index)
|
|
{
|
|
if (index > int.MaxValue)
|
|
return CurrentResult(RuntimeCommandStatus.Rejected);
|
|
return Invoke((commands, generation) =>
|
|
commands.InventoryState.RemoveShortcut(
|
|
generation,
|
|
(int)index));
|
|
}
|
|
|
|
public RuntimeCommandResult AddFavorite(
|
|
int tab,
|
|
int position,
|
|
uint spellId) =>
|
|
Invoke((commands, generation) => commands.Spellbook.AddFavorite(
|
|
generation,
|
|
tab,
|
|
position,
|
|
spellId));
|
|
|
|
public RuntimeCommandResult RemoveFavorite(int tab, uint spellId) =>
|
|
Invoke((commands, generation) => commands.Spellbook.RemoveFavorite(
|
|
generation,
|
|
tab,
|
|
spellId));
|
|
|
|
public RuntimeCommandResult SetSpellbookFilter(uint filters) =>
|
|
Invoke((commands, generation) => commands.Spellbook.SetFilter(
|
|
generation,
|
|
filters));
|
|
|
|
public RuntimeCommandResult ForgetSpell(uint spellId) =>
|
|
Invoke((commands, generation) => commands.Spellbook.ForgetSpell(
|
|
generation,
|
|
spellId));
|
|
|
|
public RuntimeCommandResult SetDesiredComponent(
|
|
uint componentId,
|
|
uint amount) =>
|
|
Invoke((commands, generation) =>
|
|
commands.Spellbook.SetDesiredComponent(
|
|
generation,
|
|
componentId,
|
|
amount));
|
|
|
|
public RuntimeCommandResult Advance(
|
|
RuntimeAdvancementKind kind,
|
|
uint statId,
|
|
ulong cost) =>
|
|
Invoke((commands, generation) => commands.Character.Advance(
|
|
generation,
|
|
new RuntimeAdvancementCommand(kind, statId, cost)));
|
|
|
|
// Campaign LA slice LA8: the retained character-management screen uses
|
|
// the same generation-capturing late seam as every gameplay panel. The
|
|
// screen never receives GameRuntime or WorldSession and cannot retain a
|
|
// stale generation across reconnect.
|
|
|
|
public RuntimeCommandResult CharacterSelectionHighlight(uint characterId) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterSelection.Highlight(generation, characterId));
|
|
|
|
public RuntimeCommandResult CharacterSelectionEnter() =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterSelection.Enter(generation));
|
|
|
|
public RuntimeCommandResult CharacterSelectionRequestDelete() =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterSelection.RequestDelete(generation));
|
|
|
|
public RuntimeCommandResult CharacterSelectionConfirmDelete() =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterSelection.ConfirmDelete(generation));
|
|
|
|
public RuntimeCommandResult CharacterSelectionRestore() =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterSelection.Restore(generation));
|
|
|
|
public RuntimeCommandResult CharacterSelectionCancel() =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterSelection.Cancel(generation));
|
|
|
|
// ── Campaign CC slice CC4: character-creation page commands ─────────
|
|
// Same "capture view+commands under one generation" shape as every
|
|
// character-selection method above.
|
|
|
|
public RuntimeCommandResult CharacterCreationSelectHeritage(uint heritageId) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.SelectHeritage(generation, heritageId));
|
|
|
|
public RuntimeCommandResult CharacterCreationSelectGender(uint genderKey) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.SelectGender(generation, genderKey));
|
|
|
|
public RuntimeCommandResult CharacterCreationSelectTemplate(uint templateIndex) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.SelectTemplate(generation, templateIndex));
|
|
|
|
public RuntimeCommandResult CharacterCreationSetAttribute(
|
|
ChargenAttributeId attributeId,
|
|
int value) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.SetAttribute(generation, attributeId, value));
|
|
|
|
public RuntimeCommandResult CharacterCreationSetAttributeLock(
|
|
ChargenAttributeId attributeId,
|
|
bool locked) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.SetAttributeLock(generation, attributeId, locked));
|
|
|
|
public RuntimeCommandResult CharacterCreationTrainSkill(uint skillId) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.TrainSkill(generation, skillId));
|
|
|
|
public RuntimeCommandResult CharacterCreationSpecializeSkill(uint skillId) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.SpecializeSkill(generation, skillId));
|
|
|
|
public RuntimeCommandResult CharacterCreationUntrainSkill(uint skillId) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.UntrainSkill(generation, skillId));
|
|
|
|
public RuntimeCommandResult CharacterCreationSelectStartArea(int startAreaIndex) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.SelectStartArea(generation, startAreaIndex));
|
|
|
|
public RuntimeCommandResult CharacterCreationFinish(bool confirmUnspentCredits) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.Finish(generation, confirmUnspentCredits));
|
|
|
|
// ── Campaign CC slice CC6b-MOUNT: Appearance page commands ───────────
|
|
|
|
public RuntimeCommandResult CharacterCreationSetAppearanceIndex(
|
|
ChargenAppearanceSlot slot,
|
|
uint index) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.SetAppearanceIndex(generation, slot, index));
|
|
|
|
public RuntimeCommandResult CharacterCreationSetShade(
|
|
ChargenShadeSlot slot,
|
|
double value) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.SetShade(generation, slot, value));
|
|
|
|
// ── Campaign CC slice CC5: Summary page + RandomizeCharacter commands ──
|
|
|
|
public RuntimeCommandResult CharacterCreationSetName(string name) =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.SetName(generation, name));
|
|
|
|
public RuntimeCommandResult CharacterCreationAcknowledgeRejection() =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.AcknowledgeRejection(generation));
|
|
|
|
public RuntimeCommandResult CharacterCreationRandomizeCharacter() =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.RandomizeCharacter(generation));
|
|
|
|
public RuntimeCommandResult CharacterCreationRandomizeAppearance() =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.RandomizeAppearance(generation));
|
|
|
|
public RuntimeCommandResult CharacterCreationRandomizeClothing() =>
|
|
Invoke((commands, generation) =>
|
|
commands.CharacterCreation.RandomizeClothing(generation));
|
|
|
|
// ── Campaign FA slice FA4: fellowship page commands ─────────────────
|
|
// Same "capture view+commands under one generation" shape as every
|
|
// method above — a displaced session (reconnect mid-click) can never
|
|
// receive a stale-generation fellowship action.
|
|
|
|
public RuntimeCommandResult FellowshipCreate(string fellowshipName, bool shareXp) =>
|
|
Invoke((commands, generation) => commands.Fellowship.Create(
|
|
generation, fellowshipName, shareXp));
|
|
|
|
public RuntimeCommandResult FellowshipRecruit(uint targetGuid) =>
|
|
Invoke((commands, generation) => commands.Fellowship.Recruit(
|
|
generation, targetGuid));
|
|
|
|
public RuntimeCommandResult FellowshipDismiss(uint targetGuid) =>
|
|
Invoke((commands, generation) => commands.Fellowship.Dismiss(
|
|
generation, targetGuid));
|
|
|
|
public RuntimeCommandResult FellowshipQuit(bool disband) =>
|
|
Invoke((commands, generation) => commands.Fellowship.Quit(
|
|
generation, disband));
|
|
|
|
public RuntimeCommandResult FellowshipAssignLeader(uint newLeaderGuid) =>
|
|
Invoke((commands, generation) => commands.Fellowship.AssignLeader(
|
|
generation, newLeaderGuid));
|
|
|
|
public RuntimeCommandResult FellowshipSetOpen(bool isOpen) =>
|
|
Invoke((commands, generation) => commands.Fellowship.SetOpen(
|
|
generation, isOpen));
|
|
|
|
/// <summary><c>0x00A6</c> — D4's panel-open/close declaration (the
|
|
/// vitals-stream gate, lane B §4.5). Idempotent at the CALLER
|
|
/// (<see cref="AcDream.App.UI.Layout.SocialFellowshipPageController"/>
|
|
/// only invokes this on an actual visibility transition); this method
|
|
/// itself just forwards, exactly like every sibling command — a call
|
|
/// while disconnected returns <see cref="RuntimeCommandStatus.Inactive"/>
|
|
/// from <see cref="Invoke"/> without sending anything.</summary>
|
|
public RuntimeCommandResult FellowshipSetPanelOpen(bool panelOpen) =>
|
|
Invoke((commands, generation) => commands.Fellowship.SetPanelOpen(
|
|
generation, panelOpen));
|
|
|
|
// ── Campaign FA slice FA5: allegiance page commands ─────────────────
|
|
// Same shape as the fellowship block above.
|
|
|
|
public RuntimeCommandResult AllegianceSwear(uint patronGuid) =>
|
|
Invoke((commands, generation) => commands.Allegiance.Swear(
|
|
generation, patronGuid));
|
|
|
|
public RuntimeCommandResult AllegianceBreak(uint targetGuid) =>
|
|
Invoke((commands, generation) => commands.Allegiance.Break(
|
|
generation, targetGuid));
|
|
|
|
public RuntimeCommandResult AllegianceKick(uint vassalGuid) =>
|
|
Invoke((commands, generation) => commands.Allegiance.Kick(
|
|
generation, vassalGuid));
|
|
|
|
/// <summary><c>0x001F</c> — CF-1's data subscription toggle (the
|
|
/// allegiance-page analogue of <see cref="FellowshipSetPanelOpen"/>'s
|
|
/// <c>0x00A6</c>). See <see cref="AcDream.App.UI.Layout.SocialAllegiancePageController"/>
|
|
/// for the three retail arming points this forwards.</summary>
|
|
public RuntimeCommandResult AllegianceSetUpdateSubscription(bool on) =>
|
|
Invoke((commands, generation) => commands.Allegiance.SetUpdateSubscription(
|
|
generation, on));
|
|
|
|
public void Deactivate()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
_deactivated = true;
|
|
_view = null;
|
|
_commands = null;
|
|
}
|
|
}
|
|
|
|
private RuntimeCommandResult Invoke(
|
|
Func<
|
|
IGameRuntimeCommands,
|
|
RuntimeGenerationToken,
|
|
RuntimeCommandResult> invoke)
|
|
{
|
|
IGameRuntimeCommands commands;
|
|
RuntimeGenerationToken generation;
|
|
lock (_gate)
|
|
{
|
|
if (_deactivated || _view is null || _commands is null)
|
|
{
|
|
return new RuntimeCommandResult(
|
|
RuntimeCommandStatus.Inactive,
|
|
_view?.Generation ?? default);
|
|
}
|
|
commands = _commands;
|
|
generation = _view.Generation;
|
|
}
|
|
return invoke(commands, generation);
|
|
}
|
|
|
|
private RuntimeCommandResult CurrentResult(RuntimeCommandStatus status)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return new RuntimeCommandResult(
|
|
status,
|
|
_view?.Generation ?? default);
|
|
}
|
|
}
|
|
|
|
private void Release(
|
|
IGameRuntimeView expectedView,
|
|
IGameRuntimeCommands expectedCommands)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
if (!ReferenceEquals(_view, expectedView)
|
|
|| !ReferenceEquals(_commands, expectedCommands))
|
|
{
|
|
return;
|
|
}
|
|
_view = null;
|
|
_commands = null;
|
|
}
|
|
}
|
|
|
|
private sealed class ExpectedRuntimeBinding(
|
|
DeferredGameRuntimeStateCommands owner,
|
|
IGameRuntimeView view,
|
|
IGameRuntimeCommands commands)
|
|
: IDisposable
|
|
{
|
|
private DeferredGameRuntimeStateCommands? _owner = owner;
|
|
|
|
public void Dispose() =>
|
|
Interlocked.Exchange(ref _owner, null)?.Release(view, commands);
|
|
}
|
|
}
|
|
|
|
/// <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 RequestUse(uint guid, ItemUseRequestReservation reservation)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reservation);
|
|
SelectionInteractionController? target;
|
|
lock (_gate)
|
|
target = !_deactivated ? _interactions : null;
|
|
if (target is null)
|
|
reservation.CancelBeforeDispatch();
|
|
else
|
|
target.RequestUse(guid, reservation);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <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 TryRequestCheckpoint(
|
|
string name,
|
|
out IRetailUiAutomationCheckpoint? checkpoint,
|
|
out string error)
|
|
{
|
|
if (!_deactivated && _target is { } target)
|
|
return target.TryRequestCheckpoint(name, out checkpoint, out error);
|
|
checkpoint = null;
|
|
error = "world lifecycle automation is not bound";
|
|
return false;
|
|
}
|
|
|
|
public void CancelCheckpoint(IRetailUiAutomationCheckpoint checkpoint)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(checkpoint);
|
|
if (!_deactivated && _target is { } target)
|
|
target.CancelCheckpoint(checkpoint);
|
|
}
|
|
|
|
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);
|
|
}
|