refactor(app): compose session and player startup

Move streaming, live-session, hydration, local-player, combat, and teleport construction behind the typed Phase-7 boundary. Add exact-owner runtime bindings and focused spawn-claim classification so partial startup rolls back without retaining old session targets while preserving the accepted construction and frame dependencies.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 18:28:32 +02:00
parent 3573da12e1
commit 7771c07fb6
31 changed files with 1759 additions and 532 deletions

View file

@ -0,0 +1,749 @@
using AcDream.App.Combat;
using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
using AcDream.App.Settings;
using AcDream.App.Streaming;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Core.Plugins;
using AcDream.Core.Rendering;
using AcDream.Core.Selection;
using AcDream.Core.World;
using Silk.NET.Windowing;
namespace AcDream.App.Composition;
internal sealed record SessionPlayerDependencies(
RuntimeOptions Options,
IWindow Window,
object DatLock,
RuntimeSettingsController Settings,
SettingsDevToolsResult SettingsDevTools,
PhysicsEngine PhysicsEngine,
PhysicsDataCache PhysicsDataCache,
WorldGameState WorldGameState,
WorldEvents WorldEvents,
SelectionState Selection,
ClientObjectTable Objects,
EntityClassificationCache ClassificationCache,
LiveEntityRuntimeSlot RuntimeSlot,
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> AnimatedEntities,
RemoteMovementObservationTracker RemoteMovementObservations,
RemotePhysicsUpdater RemotePhysicsUpdater,
RemoteInboundMotionDispatcher RemoteInboundMotion,
RetailInboundEventDispatcher InboundEntityEvents,
DeferredLiveEntityMotionRuntimeBindings MotionBindings,
LocalPlayerIdentityState PlayerIdentity,
LocalPlayerControllerSlot PlayerController,
LocalPlayerPhysicsHostSlot PlayerHost,
LocalPlayerModeState PlayerMode,
ChaseCameraInputState ChaseCameraInput,
LocalPlayerOutboundController PlayerOutbound,
DeferredLocalPlayerTeleportNetworkSink TeleportSink,
LiveWorldOriginState WorldOrigin,
WorldRenderRangeState RenderRange,
LocalPlayerShadowState PlayerShadow,
LocalPlayerSkillState PlayerSkills,
ViewportAspectState ViewportAspect,
PlayerApproachCompletionState PlayerApproachCompletions,
PointerPositionState PointerPosition,
DispatcherMovementInputSource MovementInput,
IInputCaptureSource InputCapture,
AcDream.App.Rendering.Vfx.ParticleVisibilityController ParticleVisibility,
TranslucencyFadeManager TranslucencyFades,
EntityEffectPoseRegistry EffectPoses,
UpdateFrameClock UpdateClock,
MovementTruthDiagnosticController MovementDiagnostics,
CombatAttackOperationsSlot CombatAttackOperations,
CombatState Combat,
CombatFeedbackSlot CombatFeedback,
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
Action<string> Log);
internal sealed record SessionPlayerResult(
LandblockStreamer Streamer,
StreamingController Streaming,
StreamingOriginRecenterCoordinator StreamingOriginRecenter,
WorldRevealCoordinator WorldReveal,
DatSpawnClaimHydrationClassifier SpawnClaimHydration,
LiveSessionController LiveSession,
LiveEntityHydrationController Hydration,
LiveEntityNetworkUpdateController NetworkUpdates,
LiveEntityLivenessController Liveness,
LiveEntitySessionController SessionEvents,
GameplayInputFrameController GameplayInput,
StreamingFrameController StreamingFrame,
LocalPlayerAnimationController LocalPlayerAnimation,
LocalPlayerShadowSynchronizer LocalPlayerShadow,
LiveLocalPlayerFrameRuntime LocalPlayerFrameRuntime,
RetailLocalPlayerFrameController LocalPlayerFrame,
LiveSpatialPresentationReconciler LiveSpatialReconciler,
LiveObjectFrameController LiveObjectFrame,
PlayerModeController PlayerMode,
PlayerModeAutoEntry PlayerModeAutoEntry,
LocalPlayerTeleportController LocalTeleport,
SessionPlayerRuntimeBindings RuntimeBindings);
internal interface IGameWindowSessionPlayerPublication
{
void PublishSessionPlayer(SessionPlayerResult result);
}
internal enum SessionPlayerCompositionPoint
{
StreamingRadiiResolved,
StreamerCreated,
StreamerStarted,
StreamingCreated,
RuntimeSettingsBound,
WorldRevealCreated,
LiveSessionCreated,
HydrationCreated,
LiveEntityGraphBound,
CombatOperationsBound,
GameplayInputBound,
UpdateLeavesCreated,
PlayerModeBound,
PortalTransferred,
TeleportBound,
ResultPublished,
}
internal sealed class SessionPlayerCompositionPhase
: ISessionPlayerCompositionPhase<
HostInputCameraResult,
ContentEffectsAudioResult,
WorldRenderResult,
InteractionRetainedUiResult,
LivePresentationResult,
SessionPlayerResult>
{
private readonly SessionPlayerDependencies _dependencies;
private readonly IGameWindowSessionPlayerPublication _publication;
private readonly Action<SessionPlayerCompositionPoint>? _faultInjection;
public SessionPlayerCompositionPhase(
SessionPlayerDependencies dependencies,
IGameWindowSessionPlayerPublication publication,
Action<SessionPlayerCompositionPoint>? faultInjection = null)
{
_dependencies = dependencies
?? throw new ArgumentNullException(nameof(dependencies));
_publication = publication
?? throw new ArgumentNullException(nameof(publication));
_faultInjection = faultInjection;
}
public SessionPlayerResult Compose(
HostInputCameraResult host,
ContentEffectsAudioResult content,
WorldRenderResult world,
InteractionRetainedUiResult interaction,
LivePresentationResult live)
{
ArgumentNullException.ThrowIfNull(host);
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(world);
ArgumentNullException.ThrowIfNull(interaction);
ArgumentNullException.ThrowIfNull(live);
var scope = new CompositionAcquisitionScope();
SessionPlayerRuntimeBindings? bindings = null;
bool bindingsOwnedByScope = false;
try
{
SessionPlayerResult result = ComposeCore(
host,
content,
world,
interaction,
live,
scope,
ref bindings,
ref bindingsOwnedByScope);
scope.Complete();
return result;
}
catch (Exception failure)
{
if (bindings is not null && !bindingsOwnedByScope)
{
scope.Own(
"session/player runtime bindings",
bindings,
static value => value.Dispose());
bindingsOwnedByScope = true;
}
scope.RollbackAndThrow(failure);
throw new System.Diagnostics.UnreachableException();
}
}
private SessionPlayerResult ComposeCore(
HostInputCameraResult host,
ContentEffectsAudioResult content,
WorldRenderResult world,
InteractionRetainedUiResult interaction,
LivePresentationResult live,
CompositionAcquisitionScope scope,
ref SessionPlayerRuntimeBindings? bindings,
ref bool bindingsOwnedByScope)
{
SessionPlayerDependencies d = _dependencies;
WorldRenderFoundation foundation = world.Foundation;
int nearRadius = d.Settings.ResolvedQuality.NearRadius;
int farRadius = d.Settings.ResolvedQuality.FarRadius;
if (d.Options.LegacyStreamRadius is { } legacyRadius)
{
nearRadius = legacyRadius;
farRadius = Math.Max(legacyRadius, farRadius);
}
d.RenderRange.NearRadius = nearRadius;
d.RenderRange.FarRadius = farRadius;
d.Log(
$"streaming: nearRadius={nearRadius} " +
$"(window={2 * nearRadius + 1}x{2 * nearRadius + 1}) " +
$"farRadius={farRadius} " +
$"(window={2 * farRadius + 1}x{2 * farRadius + 1})");
Fault(SessionPlayerCompositionPoint.StreamingRadiiResolved);
var landblockBuildFactory = new LandblockBuildFactory(
content.Dats,
d.DatLock,
world.TerrainBuild.HeightTable,
d.PhysicsDataCache,
d.Options.DumpSceneryZ);
var streamerLease = scope.Acquire(
"landblock streamer",
() => LandblockStreamer.CreateForRequests(
landblockBuildFactory.Build,
(id, landblock) =>
{
if (landblock is null)
return null;
uint x = (id >> 24) & 0xFFu;
uint y = (id >> 16) & 0xFFu;
return AcDream.Core.Terrain.LandblockMesh.Build(
landblock.Heightmap,
x,
y,
world.TerrainBuild.HeightTable,
world.TerrainBuild.Blending,
world.TerrainBuild.SurfaceCache);
}),
static value => value.Dispose());
Fault(SessionPlayerCompositionPoint.StreamerCreated);
streamerLease.Resource.Start();
Fault(SessionPlayerCompositionPoint.StreamerStarted);
var streaming = new StreamingController(
enqueueLoad: (id, kind, generation) => streamerLease.Resource.EnqueueLoad(
new LandblockBuildRequest(
id,
kind,
generation,
new LandblockBuildOrigin(
d.WorldOrigin.CenterX,
d.WorldOrigin.CenterY))),
enqueueUnload: streamerLease.Resource.EnqueueUnload,
drainCompletions: streamerLease.Resource.DrainCompletions,
state: live.WorldState,
nearRadius: nearRadius,
farRadius: farRadius,
presentationPipeline: live.LandblockPipeline,
clearPendingLoads: streamerLease.Resource.ClearPendingLoads);
var streamingOriginRecenter = new StreamingOriginRecenterCoordinator(
streaming,
d.WorldOrigin);
streaming.MaxCompletionsPerFrame =
d.Settings.ResolvedQuality.MaxCompletionsPerFrame;
Fault(SessionPlayerCompositionPoint.StreamingCreated);
bindings = new SessionPlayerRuntimeBindings();
var settingsTargets = new RuntimeSettingsTargets(
new SilkRuntimeDisplayWindowTarget(d.Window),
live.DrawDispatcher,
foundation.TerrainAtlas,
streaming,
d.RenderRange,
interaction.RetainedUi?.Host.Root,
d.Log);
bindings.Adopt(
"runtime settings targets",
d.Settings.BindRuntimeTargetsOwned(settingsTargets));
Fault(SessionPlayerCompositionPoint.RuntimeSettingsBound);
var spawnClaimClassifier = new DatSpawnClaimHydrationClassifier(
content.Dats,
d.DatLock);
var worldReveal = new WorldRevealCoordinator(
streaming.IsRenderNeighborhoodResident,
d.PhysicsEngine.IsSpawnCellReady,
d.PhysicsEngine.IsNeighborhoodTerrainResident,
() => live.DrawDispatcher.CompositeTexturesReady,
(destinationCell, radius) => live.DrawDispatcher.PrepareCompositeTextures(
live.WorldState.Entities,
live.WorldState.FlatViewGeneration,
destinationCell,
radius),
live.DrawDispatcher.InvalidateCompositeWarmupReadiness,
spawnClaimClassifier.IsUnhydratable,
d.Log);
Fault(SessionPlayerCompositionPoint.WorldRevealCreated);
return CompleteSessionPlayer(
host,
content,
world,
interaction,
live,
scope,
streamerLease,
streaming,
streamingOriginRecenter,
worldReveal,
spawnClaimClassifier,
bindings,
ref bindingsOwnedByScope);
}
private SessionPlayerResult CompleteSessionPlayer(
HostInputCameraResult host,
ContentEffectsAudioResult content,
WorldRenderResult world,
InteractionRetainedUiResult interaction,
LivePresentationResult live,
CompositionAcquisitionScope scope,
CompositionAcquisitionScope.CompositionAcquisitionLease<LandblockStreamer>
streamerLease,
StreamingController streaming,
StreamingOriginRecenterCoordinator streamingOriginRecenter,
WorldRevealCoordinator worldReveal,
DatSpawnClaimHydrationClassifier spawnClaimClassifier,
SessionPlayerRuntimeBindings bindings,
ref bool bindingsOwnedByScope)
{
SessionPlayerDependencies d = _dependencies;
var networkUpdateBridge = new DeferredLiveEntityNetworkUpdateSink();
var sealedDungeonCells = new DatSealedDungeonCellClassifier(
content.Dats,
d.DatLock);
var projectionMaterializer = new DatLiveEntityProjectionMaterializer(
d.Options,
content.Dats,
live.LiveEntities,
d.PhysicsDataCache,
content.AnimationLoader,
live.EntitySpawnAdapter,
world.Foundation.TextureCache,
d.ClassificationCache,
d.EffectPoses,
live.EquippedChildren,
d.WorldGameState,
d.WorldEvents,
d.PhysicsEngine.ShadowObjects,
content.CollisionBuilder,
live.ProjectileController,
live.AnimationPresenter,
live.StaticAnimationScheduler,
d.WorldOrigin,
d.UpdateClock);
var originCoordinator = new LiveEntityWorldOriginCoordinator(
d.WorldOrigin,
streaming,
live.WorldState,
worldReveal,
d.PlayerIdentity,
sealedDungeonCells,
d.Log);
var liveSessionLease = scope.Acquire(
"live-session controller",
static () => new LiveSessionController(),
static value => value.Dispose());
LiveSessionController liveSession = liveSessionLease.Resource;
bindings.Adopt(
"retained-UI live session",
interaction.LateBindings.Session.Bind(liveSession));
var localPhysicsTimestamps =
new LiveSessionLocalPhysicsTimestampPublisher(
d.PlayerIdentity,
liveSession);
Fault(SessionPlayerCompositionPoint.LiveSessionCreated);
var teardown = new LiveEntityRuntimeTeardownController(
live.LiveEntities,
live.Presentation,
live.EntityEffects,
live.RemoteTeleport,
live.SelectionInteractions,
d.Selection,
d.AnimatedEntities,
d.RemoteMovementObservations,
d.TranslucencyFades,
live.ProjectionWithdrawal,
live.EquippedChildren,
d.PhysicsEngine.ShadowObjects,
live.Lights,
d.ClassificationCache,
d.PlayerIdentity);
var deletion = new LiveEntityDeletionController(
live.LiveEntities,
d.Objects,
teardown,
d.PlayerIdentity,
d.Options.DumpLiveSpawns ? d.Log : null);
var hydration = new LiveEntityHydrationController(
live.LiveEntities,
d.Objects,
d.DatLock,
projectionMaterializer,
new LiveEntityRelationshipProjection(live.EquippedChildren),
new LiveEntityReadyPublisher(
live.LiveEntities,
live.EntityEffects,
live.Presentation),
originCoordinator,
networkUpdateBridge,
localPhysicsTimestamps,
d.PlayerIdentity,
deletion,
d.Options.DumpLiveSpawns ? d.Log : null);
bindings.Adopt(
"landblock-loaded hydration",
live.LandblockLoaded.Bind(hydration));
Fault(SessionPlayerCompositionPoint.HydrationCreated);
var networkUpdates = new LiveEntityNetworkUpdateController(
live.LiveEntities,
d.Objects,
hydration,
live.EntityEffects,
live.Presentation,
live.Lights,
live.EquippedChildren,
live.ProjectileController,
live.RemoteTeleport,
d.AnimatedEntities,
new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(d.RuntimeSlot),
d.RemoteMovementObservations,
d.RemotePhysicsUpdater,
d.RemoteInboundMotion,
live.MotionRuntime,
d.PhysicsEngine,
content.Dats,
content.AnimationLoader,
interaction.CombatTarget,
d.WorldOrigin,
d.TeleportSink,
d.PlayerController,
d.PlayerOutbound,
d.PlayerHost,
d.PlayerIdentity,
d.UpdateClock,
liveSession,
localPhysicsTimestamps.Publish,
d.MovementDiagnostics);
var liveness = new LiveEntityLivenessController(
live.LiveEntities,
d.PlayerIdentity,
deletion);
var sessionEvents = new LiveEntitySessionController(
d.InboundEntityEvents,
hydration,
networkUpdates,
d.TeleportSink,
live.EntityEffects);
bindings.Adopt(
"live parent acceptance",
live.ParentAcceptance.BindOwned(
hydration.TryAcceptParentForProjection));
bindings.Adopt(
"same-generation network updates",
networkUpdateBridge.BindOwned(networkUpdates));
bindings.BindEntityReady(
live.EquippedChildren,
candidate => _ = hydration.OnEntityReady(candidate));
bindings.BindAppearanceApplied(
hydration,
guid =>
{
if (guid == d.PlayerIdentity.ServerGuid)
live.PaperdollPresenter?.MarkDirty();
});
Fault(SessionPlayerCompositionPoint.LiveEntityGraphBound);
d.WorldOrigin.SetPlaceholder(
world.TerrainBuild.InitialCenterX,
world.TerrainBuild.InitialCenterY);
bindings.Adopt(
"combat attack operations",
d.CombatAttackOperations.BindOwned(
new LiveCombatAttackOperations(
d.Combat,
new CombatAttackTargetSource(
d.Selection,
live.LiveEntities,
d.Objects,
d.PlayerIdentity),
d.Settings,
d.PlayerController,
d.PlayerOutbound,
liveSession,
liveSession,
d.CombatFeedback)));
Fault(SessionPlayerCompositionPoint.CombatOperationsBound);
MouseLookController? mouseLook =
host.MouseSource is not null && host.MouseLookCursor is not null
? new MouseLookController(
host.MouseSource,
d.PointerPosition,
d.PlayerMode,
d.PlayerController,
host.CameraController,
d.ChaseCameraInput,
d.MovementInput,
d.PlayerOutbound,
liveSession,
host.MouseLookCursor,
new EnvironmentInputMonotonicClock())
: null;
var gameplayInput = new GameplayInputFrameController(
host.InputDispatcher,
d.MovementInput,
mouseLook,
new CombatAttackInputFrameAdapter(interaction.CombatAttack));
if (host.CameraPointerInput is { } cameraPointer)
{
bindings.Adopt(
"camera pointer gameplay frame",
cameraPointer.BindGameplayFrameOwned(gameplayInput));
}
Fault(SessionPlayerCompositionPoint.GameplayInputBound);
var streamingFrame = new StreamingFrameController(
d.Options.LiveMode,
d.PlayerMode,
d.PlayerController,
liveSession,
d.WorldOrigin,
networkUpdates,
new FlyCameraStreamingObserverSource(host.CameraController),
new PhysicsStreamingDungeonCellSource(d.PhysicsEngine),
streamingOriginRecenter,
streaming,
new LiveProjectionRescueRebucketter(
live.WorldState,
live.LiveEntities));
var liveEffectFrame = new LiveEffectFrameController(
d.TranslucencyFades,
content.AnimationHookFrames,
live.EntityEffects,
content.ParticleSink,
live.Lights,
d.ParticleVisibility,
content.ParticleSystem,
content.ScriptRunner,
d.UpdateClock,
new SettingsParticleRangeSource(d.Settings));
var liveSpatialReconciler = new LiveSpatialPresentationReconciler(
live.EntityEffects,
live.EquippedChildren,
content.ParticleSink,
live.Lights);
var localPlayerAnimation = new LocalPlayerAnimationController(
live.LiveEntities,
d.PlayerIdentity,
d.AnimatedEntities,
live.AnimationPresenter,
content.AnimationHookFrames);
var localPlayerShadow = new LocalPlayerShadowSynchronizer(
d.PhysicsEngine,
live.LiveEntities,
d.PlayerIdentity,
d.WorldOrigin,
d.PlayerShadow);
var localPlayerProjection = new LocalPlayerProjectionController(
new LiveLocalPlayerProjectionRuntime(
live.LiveEntities,
d.PlayerIdentity,
d.WorldOrigin,
localPlayerShadow));
var localPlayerFrameRuntime = new LiveLocalPlayerFrameRuntime(
host.CameraController,
d.PlayerMode,
d.PlayerController,
d.ChaseCameraInput,
d.MovementInput,
d.InputCapture,
live.LiveEntities,
d.PlayerIdentity,
d.PlayerHost,
localPlayerProjection,
d.PlayerOutbound,
liveSession);
var localPlayerFrame = new RetailLocalPlayerFrameController(
localPlayerFrameRuntime,
d.MovementInput);
var liveObjectFrame = new LiveObjectFrameController(
d.InboundEntityEvents,
localPlayerFrame,
live.SelectionInteractions,
live.LiveEntities,
d.PlayerIdentity,
d.WorldOrigin,
live.AnimationScheduler,
live.StaticAnimationScheduler,
live.AnimationPresenter,
d.AnimatedEntities,
live.EquippedChildren,
liveEffectFrame);
Fault(SessionPlayerCompositionPoint.UpdateLeavesCreated);
var playerMode = new PlayerModeController(
d.PlayerMode,
d.PlayerController,
d.PlayerHost,
d.ChaseCameraInput,
host.CameraController,
d.PhysicsEngine,
live.LiveEntities,
d.PlayerIdentity,
d.WorldOrigin,
d.MotionBindings,
content.Dats,
d.DatLock,
d.PhysicsDataCache,
d.AnimatedEntities,
localPlayerAnimation,
localPlayerShadow,
d.PlayerApproachCompletions,
gameplayInput,
liveSession,
d.MovementDiagnostics,
d.PlayerSkills,
d.ViewportAspect);
if (d.SettingsDevTools.DevTools is { } devTools)
{
bindings.Adopt(
"developer player mode",
devTools.LateBindings.PlayerModeCommands.BindOwned(playerMode));
bindings.Adopt(
"developer command bus",
devTools.CommandBus.BindOwned(liveSession));
}
var playerModeAutoEntry = new PlayerModeAutoEntry(
new LivePlayerModeAutoEntryContext(
liveSession,
live.LiveEntities,
d.PlayerIdentity,
worldReveal,
d.PlayerMode,
playerMode));
playerMode.BindAutoEntry(playerModeAutoEntry);
Fault(SessionPlayerCompositionPoint.PlayerModeBound);
LocalPlayerTeleportController localTeleport =
d.PortalTunnelFallback.Transfer(
portalTunnel => new LocalPlayerTeleportController(
new LiveLocalPlayerTeleportAuthority(
live.LiveEntities,
d.PlayerIdentity),
gameplayInput,
playerMode,
new LocalPlayerTeleportStreamingOperations(
d.WorldOrigin,
streamingOriginRecenter,
streaming,
sealedDungeonCells),
worldReveal,
new LocalPlayerTeleportPlacement(
d.PhysicsEngine,
live.LiveEntities,
d.PlayerIdentity,
d.PlayerController,
d.PlayerHost,
d.ChaseCameraInput,
d.WorldOrigin,
liveSpatialReconciler),
new LocalPlayerTeleportSession(liveSession),
new LocalPlayerTeleportPresentation(portalTunnel)));
var teleportLease = scope.Own(
"local-player teleport",
localTeleport,
static value => value.Dispose());
Fault(SessionPlayerCompositionPoint.PortalTransferred);
bindings.Adopt(
"local teleport network sink",
d.TeleportSink.BindOwned(localTeleport));
bindings.Adopt(
"selection view plane",
interaction.LateBindings.SelectionViewPlane.Bind(localTeleport));
Fault(SessionPlayerCompositionPoint.TeleportBound);
IDisposable componentLifecycleBinding =
live.ComponentLifecycle.BindOwned(teardown);
try
{
live.RuntimeBindings.Adopt(
"live runtime component teardown",
componentLifecycleBinding);
}
catch
{
componentLifecycleBinding.Dispose();
throw;
}
var bindingsLease = scope.Own(
"session/player runtime bindings",
bindings,
static value => value.Dispose());
bindingsOwnedByScope = true;
var result = new SessionPlayerResult(
streamerLease.Resource,
streaming,
streamingOriginRecenter,
worldReveal,
spawnClaimClassifier,
liveSession,
hydration,
networkUpdates,
liveness,
sessionEvents,
gameplayInput,
streamingFrame,
localPlayerAnimation,
localPlayerShadow,
localPlayerFrameRuntime,
localPlayerFrame,
liveSpatialReconciler,
liveObjectFrame,
playerMode,
playerModeAutoEntry,
teleportLease.Resource,
bindings);
_publication.PublishSessionPlayer(result);
streamerLease.Transfer();
liveSessionLease.Transfer();
teleportLease.Transfer();
bindingsLease.Transfer();
Fault(SessionPlayerCompositionPoint.ResultPublished);
return result;
}
private void Fault(SessionPlayerCompositionPoint point) =>
_faultInjection?.Invoke(point);
}