refactor(app): complete session startup composition
Move the live-session reset and routing graph, combat and diagnostic command targets, and the sole gameplay input subscriber into Phase 7 before frame publication. Add exact retryable ownership for late bindings so partial startup cannot strand session or component teardown edges. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
7fa60971e2
commit
826f9ea9b5
22 changed files with 924 additions and 412 deletions
|
|
@ -406,6 +406,20 @@ internal sealed class ShortcutSnapshotState
|
|||
}
|
||||
}
|
||||
|
||||
internal sealed class DesiredComponentSnapshotState
|
||||
{
|
||||
private IReadOnlyList<(uint Id, uint Amount)> _items =
|
||||
Array.Empty<(uint Id, uint Amount)>();
|
||||
|
||||
public IReadOnlyList<(uint Id, uint Amount)> Items
|
||||
{
|
||||
get => _items;
|
||||
set => _items = value ?? Array.Empty<(uint Id, uint Amount)>();
|
||||
}
|
||||
|
||||
public void Clear() => _items = Array.Empty<(uint Id, uint Amount)>();
|
||||
}
|
||||
|
||||
/// <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.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ namespace AcDream.App.Composition;
|
|||
/// </summary>
|
||||
internal sealed class LivePresentationRuntimeBindings : IDisposable
|
||||
{
|
||||
private readonly List<(string Name, IDisposable Binding)> _bindings = [];
|
||||
private sealed record Entry(string Name, IDisposable Binding);
|
||||
|
||||
private readonly List<Entry> _bindings = [];
|
||||
private bool _deactivationStarted;
|
||||
|
||||
public void Adopt(string name, IDisposable binding)
|
||||
|
|
@ -18,7 +20,17 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
|
|||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
ArgumentNullException.ThrowIfNull(binding);
|
||||
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
||||
_bindings.Add((name, binding));
|
||||
_bindings.Add(new Entry(name, binding));
|
||||
}
|
||||
|
||||
public IDisposable AdoptOwned(string name, IDisposable binding)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
ArgumentNullException.ThrowIfNull(binding);
|
||||
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
||||
var entry = new Entry(name, binding);
|
||||
_bindings.Add(entry);
|
||||
return new Adoption(this, entry);
|
||||
}
|
||||
|
||||
public void AdoptRelease(string name, Action release) =>
|
||||
|
|
@ -56,16 +68,16 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
|
|||
List<Exception>? failures = null;
|
||||
for (int i = _bindings.Count - 1; i >= 0; i--)
|
||||
{
|
||||
(string name, IDisposable binding) = _bindings[i];
|
||||
Entry entry = _bindings[i];
|
||||
try
|
||||
{
|
||||
binding.Dispose();
|
||||
entry.Binding.Dispose();
|
||||
_bindings.RemoveAt(i);
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
(failures ??= []).Add(new InvalidOperationException(
|
||||
$"Live-presentation binding '{name}' did not detach.",
|
||||
$"Live-presentation binding '{entry.Name}' did not detach.",
|
||||
failure));
|
||||
}
|
||||
}
|
||||
|
|
@ -78,6 +90,38 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void ReleaseAdoption(Entry expected)
|
||||
{
|
||||
int index = _bindings.IndexOf(expected);
|
||||
if (index < 0)
|
||||
return;
|
||||
expected.Binding.Dispose();
|
||||
_bindings.RemoveAt(index);
|
||||
}
|
||||
|
||||
private sealed class Adoption : IDisposable
|
||||
{
|
||||
private LivePresentationRuntimeBindings? _owner;
|
||||
private readonly Entry _expected;
|
||||
|
||||
public Adoption(
|
||||
LivePresentationRuntimeBindings owner,
|
||||
Entry expected)
|
||||
{
|
||||
_owner = owner;
|
||||
_expected = expected;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
LivePresentationRuntimeBindings? owner = _owner;
|
||||
if (owner is null)
|
||||
return;
|
||||
owner.ReleaseAdoption(_expected);
|
||||
_owner = null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DelegateBinding : IDisposable
|
||||
{
|
||||
private Action? _release;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering;
|
||||
|
|
@ -12,11 +13,15 @@ using AcDream.App.Update;
|
|||
using AcDream.App.World;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Core.Plugins;
|
||||
using AcDream.Core.Rendering;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Player;
|
||||
using AcDream.Core.Social;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Core.World;
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
|
|
@ -28,6 +33,11 @@ internal sealed record SessionPlayerDependencies(
|
|||
object DatLock,
|
||||
RuntimeSettingsController Settings,
|
||||
SettingsDevToolsResult SettingsDevTools,
|
||||
WorldEnvironmentController WorldEnvironment,
|
||||
WorldSceneDebugState WorldSceneDebugState,
|
||||
RuntimeDiagnosticCommandSlot RuntimeDiagnosticCommands,
|
||||
LiveCombatModeCommandSlot CombatModeCommands,
|
||||
HostQuiescenceGate HostQuiescence,
|
||||
PhysicsEngine PhysicsEngine,
|
||||
PhysicsDataCache PhysicsDataCache,
|
||||
WorldGameState WorldGameState,
|
||||
|
|
@ -55,6 +65,9 @@ internal sealed record SessionPlayerDependencies(
|
|||
LocalPlayerSkillState PlayerSkills,
|
||||
ViewportAspectState ViewportAspect,
|
||||
PlayerApproachCompletionState PlayerApproachCompletions,
|
||||
PlayerCharacterOptionsState CharacterOptions,
|
||||
ShortcutSnapshotState Shortcuts,
|
||||
DesiredComponentSnapshotState DesiredComponents,
|
||||
PointerPositionState PointerPosition,
|
||||
DispatcherMovementInputSource MovementInput,
|
||||
IInputCaptureSource InputCapture,
|
||||
|
|
@ -66,6 +79,14 @@ internal sealed record SessionPlayerDependencies(
|
|||
CombatAttackOperationsSlot CombatAttackOperations,
|
||||
CombatState Combat,
|
||||
CombatFeedbackSlot CombatFeedback,
|
||||
ChatLog Chat,
|
||||
LocalPlayerState LocalPlayer,
|
||||
Spellbook Spellbook,
|
||||
ItemManaState ItemMana,
|
||||
FriendsState Friends,
|
||||
SquelchState Squelch,
|
||||
TurbineChatState TurbineChat,
|
||||
ExternalContainerState ExternalContainers,
|
||||
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
||||
Action<string> Log);
|
||||
|
||||
|
|
@ -91,6 +112,8 @@ internal sealed record SessionPlayerResult(
|
|||
PlayerModeController PlayerMode,
|
||||
PlayerModeAutoEntry PlayerModeAutoEntry,
|
||||
LocalPlayerTeleportController LocalTeleport,
|
||||
LiveSessionHost SessionHost,
|
||||
GameplayInputActionRouter? GameplayActions,
|
||||
SessionPlayerRuntimeBindings RuntimeBindings);
|
||||
|
||||
internal interface IGameWindowSessionPlayerPublication
|
||||
|
|
@ -115,6 +138,9 @@ internal enum SessionPlayerCompositionPoint
|
|||
PlayerModeBound,
|
||||
PortalTransferred,
|
||||
TeleportBound,
|
||||
SessionHostCreated,
|
||||
CommandsBound,
|
||||
GameplayActionsAttached,
|
||||
ResultPublished,
|
||||
}
|
||||
|
||||
|
|
@ -694,9 +720,10 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
|
||||
IDisposable componentLifecycleBinding =
|
||||
live.ComponentLifecycle.BindOwned(teardown);
|
||||
IDisposable componentLifecycleAdoption;
|
||||
try
|
||||
{
|
||||
live.RuntimeBindings.Adopt(
|
||||
componentLifecycleAdoption = live.RuntimeBindings.AdoptOwned(
|
||||
"live runtime component teardown",
|
||||
componentLifecycleBinding);
|
||||
}
|
||||
|
|
@ -705,6 +732,153 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
componentLifecycleBinding.Dispose();
|
||||
throw;
|
||||
}
|
||||
var componentLifecycleLease = scope.Own(
|
||||
"live runtime component teardown adoption",
|
||||
componentLifecycleAdoption,
|
||||
static value => value.Dispose());
|
||||
|
||||
AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? vitals =
|
||||
d.SettingsDevTools.DevTools?.Vitals
|
||||
?? interaction.RetainedUi?.Vitals;
|
||||
AcDream.UI.Abstractions.Panels.Chat.ChatVM? retailChat =
|
||||
interaction.RetainedUi?.Chat;
|
||||
var sessionRuntimeFactory = new LiveSessionRuntimeFactory(
|
||||
new LiveSessionPlayerRuntime(
|
||||
d.PlayerIdentity,
|
||||
d.PlayerController,
|
||||
d.PlayerSkills,
|
||||
d.WorldOrigin,
|
||||
d.CharacterOptions,
|
||||
d.Shortcuts,
|
||||
d.DesiredComponents),
|
||||
new LiveSessionDomainRuntime(
|
||||
d.Objects,
|
||||
d.LocalPlayer,
|
||||
d.Spellbook,
|
||||
d.Combat,
|
||||
d.ItemMana,
|
||||
d.Friends,
|
||||
d.Squelch,
|
||||
d.TurbineChat,
|
||||
d.ExternalContainers,
|
||||
d.Chat),
|
||||
new LiveSessionUiRuntime(
|
||||
interaction.RetainedUi?.Runtime,
|
||||
vitals,
|
||||
retailChat,
|
||||
interaction.RetainedUi?.CharacterSheet,
|
||||
interaction.RetainedUi?.Magic,
|
||||
live.PaperdollPresenter),
|
||||
new LiveSessionInteractionRuntime(
|
||||
d.Settings,
|
||||
gameplayInput,
|
||||
playerMode,
|
||||
playerModeAutoEntry,
|
||||
interaction.ItemInteraction,
|
||||
interaction.CombatAttack,
|
||||
live.SelectionInteractions),
|
||||
new LiveSessionWorldRuntime(
|
||||
content.Dats,
|
||||
live.WorldState,
|
||||
live.LiveEntities,
|
||||
sessionEvents,
|
||||
d.WorldEnvironment,
|
||||
d.TeleportSink,
|
||||
spawnClaimClassifier,
|
||||
live.EquippedChildren,
|
||||
live.SelectionScene,
|
||||
d.ParticleVisibility,
|
||||
d.InboundEntityEvents,
|
||||
liveness,
|
||||
networkUpdates,
|
||||
hydration,
|
||||
live.RemoteTeleport,
|
||||
live.EntityEffects,
|
||||
content.AnimationHookFrames,
|
||||
live.Presentation,
|
||||
d.RemoteMovementObservations),
|
||||
d.Log);
|
||||
LiveSessionHost sessionHost = sessionRuntimeFactory.Create(liveSession);
|
||||
Fault(SessionPlayerCompositionPoint.SessionHostCreated);
|
||||
|
||||
Action<string>? debugToast = d.SettingsDevTools.DevTools is { } devOwner
|
||||
? message => devOwner.Debug.AddToast(message)
|
||||
: null;
|
||||
var combatCommand = new LiveCombatModeCommandController(
|
||||
new LiveSessionCombatModeAuthority(sessionHost),
|
||||
new LocalPlayerCombatEquipmentSource(
|
||||
d.Objects,
|
||||
d.PlayerIdentity),
|
||||
d.Combat,
|
||||
new ItemInteractionCombatModeIntentSink(
|
||||
interaction.ItemInteraction),
|
||||
d.Log,
|
||||
debugToast,
|
||||
text => d.Chat.OnSystemMessage(text, 0x1Au));
|
||||
bindings.Adopt(
|
||||
"live combat-mode commands",
|
||||
d.CombatModeCommands.BindOwned(combatCommand));
|
||||
|
||||
var nearbyDiagnostics = new NearbyWorldDiagnosticDumper(
|
||||
new RuntimeNearbyWorldDiagnosticSource(
|
||||
d.PlayerMode,
|
||||
d.PlayerController,
|
||||
host.CameraController,
|
||||
d.WorldOrigin,
|
||||
live.WorldState,
|
||||
d.PhysicsEngine),
|
||||
d.Log);
|
||||
var runtimeDiagnostics = new RuntimeDiagnosticCommandController(
|
||||
d.WorldEnvironment,
|
||||
d.WorldSceneDebugState,
|
||||
host.CameraPointerInput,
|
||||
nearbyDiagnostics,
|
||||
debugToast);
|
||||
bindings.Adopt(
|
||||
"runtime diagnostic commands",
|
||||
d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics));
|
||||
Fault(SessionPlayerCompositionPoint.CommandsBound);
|
||||
|
||||
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||
GameplayInputActionRouter>? gameplayActionsLease = null;
|
||||
if (host.InputDispatcher is { } dispatcher)
|
||||
{
|
||||
CameraPointerInputController pointer = host.CameraPointerInput
|
||||
?? throw new InvalidOperationException(
|
||||
"Gameplay action routing requires the composed pointer owner.");
|
||||
var commands = new GameplayInputCommandController(
|
||||
new RetainedGameplayWindowCommands(
|
||||
interaction.RetainedUi?.Runtime),
|
||||
new DevToolsGameplayCommands(
|
||||
d.SettingsDevTools.DevTools?.Presenter),
|
||||
runtimeDiagnostics,
|
||||
new PlayerModeGameplayCommands(
|
||||
d.PlayerMode,
|
||||
playerMode),
|
||||
new ItemTargetModeCommands(interaction.ItemInteraction),
|
||||
new GameplayCameraModeCommands(host.CameraController),
|
||||
combatCommand,
|
||||
new GameplayWindowCommands(d.Window.Close));
|
||||
var targets = new RuntimeGameplayInputPriorityTargets(
|
||||
gameplayInput,
|
||||
pointer,
|
||||
interaction.RetainedUi?.Runtime,
|
||||
live.SelectionInteractions,
|
||||
commands);
|
||||
GameplayInputActionRouter gameplayActions =
|
||||
GameplayInputActionRouter.Create(
|
||||
dispatcher,
|
||||
d.Combat,
|
||||
targets,
|
||||
d.HostQuiescence,
|
||||
d.Log);
|
||||
gameplayActionsLease = scope.Own(
|
||||
"gameplay input actions",
|
||||
gameplayActions,
|
||||
static value => value.Dispose());
|
||||
gameplayActions.Attach();
|
||||
}
|
||||
Fault(SessionPlayerCompositionPoint.GameplayActionsAttached);
|
||||
|
||||
var bindingsLease = scope.Own(
|
||||
"session/player runtime bindings",
|
||||
|
|
@ -733,12 +907,16 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
playerMode,
|
||||
playerModeAutoEntry,
|
||||
teleportLease.Resource,
|
||||
sessionHost,
|
||||
gameplayActionsLease?.Resource,
|
||||
bindings);
|
||||
_publication.PublishSessionPlayer(result);
|
||||
|
||||
streamerLease.Transfer();
|
||||
liveSessionLease.Transfer();
|
||||
teleportLease.Transfer();
|
||||
gameplayActionsLease?.Transfer();
|
||||
componentLifecycleLease.Transfer();
|
||||
bindingsLease.Transfer();
|
||||
Fault(SessionPlayerCompositionPoint.ResultPublished);
|
||||
return result;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue