acdream/src/AcDream.App/Composition/FrameRootComposition.cs
Erik 91c1962b0d fix #416 #415: the retail button state/media machine — roster hover highlight clears; probe wait verbs bind without an artifact dir
#416 (char-select roster highlight never cleared on hover-leave): three
decomp-grounded mechanisms replace the media-keyed _availableStates
approximation.
- UIElement_Button::UpdateState_ @0x00471CF0: the button machine commits
  ONLY states authored on the button's OWN ElementDesc (AccessStateDesc
  gate); unauthored requests no-op, preserving custom semantic states.
- UIElement::SetState @0x00464E70: an unauthored state id is coerced to
  state 0 (the unnamed base state) and committed — ported into
  UiDatElement.TrySetRetailState with the base-descriptor PassToChildren
  cascade arm.
- The SetState media rule @0x004651c0: a committed state replaces the
  playing media ONLY when its media array is non-empty. UiButton now keeps
  per-face-segment media states under that rule (segments model retail's
  PassToChildren children), and LayoutImporter records the raw MediaCount
  including the File=0 draw-nothing images the drawable filter drops —
  the roster bar children's base state is exactly such an image, and it is
  what clears the bar.
The row template truth (probe, installed DAT): the row authors EMPTY
Normal/rollover/Highlight descriptors with PassToChildren; the three bar
children author rollover/Highlight media, NO Normal state, and a File=0
base image. An empty-media Normal_pressed still never blanks a Normal-art
button (the media rule keeps the previous art — the exact behavior the
old gate approximated), and the Appearance spins' property-only Highlight
now genuinely commits: label recolors, arrow art lingers — the retail
split AP-222 approximated with a requested-keyed label hack, now retired.
Live-verified at char select: hover +alex shows the grey bar, moving off
clears it, the selected row keeps its amber bar.

#415 (probe wait world-* verbs dead): the filed snapshot-reset diagnosis
was wrong — the automation bridge simply never bound without
ACDREAM_AUTOMATION_ARTIFACT_DIR. A facts-only
WorldRevealFactsAutomationRuntime now binds whenever the retained UI
exists; checkpoint/screenshot verbs still require the artifact directory
and now report that instead of a generic timeout.

App tests 5568/3 skips, Runtime 1756/0, UI.Abstractions 926/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 16:25:36 +02:00

693 lines
29 KiB
C#

using AcDream.App.Diagnostics;
using AcDream.App.Input;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Runtime;
using AcDream.App.Settings;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Runtime;
using AcDream.Runtime.Session;
using AcDream.Core.Combat;
using AcDream.Core.Lighting;
using AcDream.Core.Physics;
using AcDream.Core.Selection;
using AcDream.Core.World;
using Silk.NET.Input;
using Silk.NET.Windowing;
namespace AcDream.App.Composition;
internal sealed record FrameRootDependencies(
RuntimeOptions Options,
GameRuntime Runtime,
GameWindowGraphics Graphics,
IWindow Window,
IInputContext Input,
WorldTimeService WorldTime,
WeatherSystem Weather,
LightManager Lighting,
WorldEnvironmentController WorldEnvironment,
PhysicsEngine PhysicsEngine,
CellVisibility CellVisibility,
LocalPlayerModeState PlayerMode,
LocalPlayerIdentityState PlayerIdentity,
ChaseCameraInputState ChaseCameraInput,
LiveWorldOriginState WorldOrigin,
ParticleVisibilityController ParticleVisibility,
EntityEffectPoseRegistry EffectPoses,
WorldRenderRangeState RenderRange,
RuntimeSettingsController Settings,
DisplayFramePacingController DisplayFramePacing,
WorldSceneDebugState WorldSceneDebugState,
RetailAlphaQueue RetailAlphaQueue,
FrameProfiler FrameProfiler,
bool FrameDiagnosticsEnabled,
IRenderFrameDiagnosticLog RenderDiagnosticLog,
DebugVmRenderFactsPublisher DebugVmRenderFacts,
IInputCaptureSource InputCapture,
DispatcherCameraInputSource CameraInput,
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> Animations,
UpdateFrameClock UpdateClock,
GameFrameGraphSlot FrameGraphs,
Action<string> Log)
{
public RuntimeLocalPlayerMovementState PlayerController =>
Runtime.MovementOwner;
public SelectionState Selection => Runtime.ActionOwner.Selection;
public CombatState Combat => Runtime.ActionOwner.Combat;
}
internal sealed record FrameRootResult(
UpdateFrameOrchestrator Update,
RenderFrameOrchestrator Render,
FrameRootRuntimeBindings RuntimeBindings,
IDisposable FrameGraphPublication,
LiveSessionHost SessionHost,
CurrentGameRuntimeAdapter GameRuntime);
internal interface IGameWindowFrameRootPublication
{
void PublishFrameRoots(FrameRootResult result);
}
internal enum FrameRootCompositionPoint
{
RenderResourcesCreated,
WorldRendererCreated,
LifecycleAutomationBound,
RenderRootCreated,
UpdateRootCreated,
FrameGraphPublished,
ResultPublished,
}
/// <summary>
/// Exact late edges created with the frame roots. Completed detach operations
/// are removed immediately so shutdown retry never replays them.
/// </summary>
internal sealed class FrameRootRuntimeBindings : IDisposable
{
private readonly List<(string Name, IDisposable Binding)> _bindings = [];
private bool _deactivationStarted;
public void Adopt(string name, IDisposable binding)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(binding);
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
_bindings.Add((name, binding));
}
public void Dispose()
{
if (_deactivationStarted && _bindings.Count == 0)
return;
_deactivationStarted = true;
List<Exception>? failures = null;
for (int i = _bindings.Count - 1; i >= 0; i--)
{
(string name, IDisposable binding) = _bindings[i];
try
{
binding.Dispose();
_bindings.RemoveAt(i);
}
catch (Exception failure)
{
(failures ??= []).Add(new InvalidOperationException(
$"Frame-root binding '{name}' did not detach.",
failure));
}
}
if (failures is not null)
{
throw new AggregateException(
"Frame-root binding cleanup remains incomplete.",
failures);
}
}
}
internal sealed class FrameRootCompositionPhase
: IFrameRootCompositionPhase<
GameWindowPlatformResult<GameWindowGraphics, IInputContext>,
HostInputCameraResult,
ContentEffectsAudioResult,
SettingsDevToolsResult,
WorldRenderResult,
InteractionRetainedUiResult,
LivePresentationResult,
SessionPlayerResult,
FrameRootResult>
{
private readonly FrameRootDependencies _dependencies;
private readonly IGameWindowFrameRootPublication _publication;
private readonly Action<FrameRootCompositionPoint>? _faultInjection;
public FrameRootCompositionPhase(
FrameRootDependencies dependencies,
IGameWindowFrameRootPublication publication,
Action<FrameRootCompositionPoint>? faultInjection = null)
{
_dependencies = dependencies
?? throw new ArgumentNullException(nameof(dependencies));
_publication = publication
?? throw new ArgumentNullException(nameof(publication));
_faultInjection = faultInjection;
}
public FrameRootResult Compose(
GameWindowPlatformResult<GameWindowGraphics, IInputContext> platform,
HostInputCameraResult host,
ContentEffectsAudioResult content,
SettingsDevToolsResult settings,
WorldRenderResult world,
InteractionRetainedUiResult interaction,
LivePresentationResult live,
SessionPlayerResult session)
{
ArgumentNullException.ThrowIfNull(platform);
ArgumentNullException.ThrowIfNull(host);
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(world);
ArgumentNullException.ThrowIfNull(interaction);
ArgumentNullException.ThrowIfNull(live);
ArgumentNullException.ThrowIfNull(session);
if (!ReferenceEquals(_dependencies.Graphics, platform.Graphics)
|| !ReferenceEquals(_dependencies.Input, platform.Input))
{
throw new InvalidOperationException(
"Frame-root dependencies do not match the ordered platform result.");
}
var scope = new CompositionAcquisitionScope();
FrameRootRuntimeBindings? bindings = null;
bool bindingsOwnedByScope = false;
try
{
FrameRootResult result = ComposeCore(
host,
content,
settings,
world,
interaction,
live,
session,
scope,
ref bindings,
ref bindingsOwnedByScope);
scope.Complete();
return result;
}
catch (Exception failure)
{
if (bindings is not null && !bindingsOwnedByScope)
{
scope.Own(
"frame-root runtime bindings",
bindings,
static value => value.Dispose());
}
scope.RollbackAndThrow(failure);
throw new System.Diagnostics.UnreachableException();
}
}
private FrameRootResult ComposeCore(
HostInputCameraResult host,
ContentEffectsAudioResult content,
SettingsDevToolsResult settings,
WorldRenderResult world,
InteractionRetainedUiResult interaction,
LivePresentationResult live,
SessionPlayerResult session,
CompositionAcquisitionScope scope,
ref FrameRootRuntimeBindings? bindings,
ref bool bindingsOwnedByScope)
{
FrameRootDependencies d = _dependencies;
WorldRenderFoundation foundation = world.Foundation;
// Campaign V slice V6h: the frame root's raw-GL render graph fork was
// deleted at slice V11. The graph is the clear pass, the private-
// presentation phase, and the retained UI inside it — the client's
// own frame, drawn entirely through the RHI.
var renderLoginState = new RenderLoginStateSource(
d.Options.LiveMode,
d.PlayerMode);
var teleportRenderState =
new LocalPlayerTeleportRenderStateSource(
session.LocalTeleport,
renderLoginState);
// Campaign V slice V6i-3: on Vulkan the frame's clear is a load op of the
// world pass rather than a pass of its own, so the two phases share this
// one value. See VulkanWorldScenePhase for why the merge is required
// rather than tidier.
var vulkanClear = new AcDream.App.Rendering.Gpu.Vk.VulkanBackbufferClearState();
var renderFrameLivePreparation =
new RuntimeRenderFrameLivePreparation(
foundation.TextureCache,
foundation.MeshAdapter,
session.WorldReveal,
teleportRenderState,
renderLoginState,
new LiveLoginRevealCellSource(
live.LiveEntities,
d.PlayerIdentity),
live.ParticleRenderer,
d.FrameProfiler,
d.FrameDiagnosticsEnabled);
IRenderFrameClearPhase clearPhase =
new AcDream.App.Rendering.Gpu.Vk.VulkanRenderFrameClearPhase(
d.WorldTime,
d.Weather,
teleportRenderState,
d.ParticleVisibility,
vulkanClear);
var renderFrameResources = new RenderFrameResourceController(
host.FrameSlots,
new RuntimeRenderFrameBeginResources(
foundation.TextureCache,
live.DrawDispatcher,
live.EnvCellRenderer,
live.PortalDepthMask,
live.ClipFrame,
foundation.Terrain,
foundation.SceneLighting),
clearPhase,
renderFrameLivePreparation);
Fault(FrameRootCompositionPoint.RenderResourcesCreated);
var renderWeatherFrame = new RenderWeatherFrameController(
d.WorldTime,
d.Weather);
var skyPesFrame = new SkyPesFrameController(
content.ScriptRunner,
content.ParticleSink,
d.EffectPoses,
live.EntityEffects);
IWorldSceneFramePhase? worldSceneRenderer = null;
CurrentRenderSceneOracle? currentRenderSceneOracle =
interaction.RetainedUi?.Screenshots is not null
&& d.Options.AutomationArtifactDirectory is not null
? new CurrentRenderSceneOracle()
: null;
RenderSceneShadowComparisonController? renderSceneShadowComparison =
currentRenderSceneOracle is not null
&& live.RenderSceneShadow is not null
? new RenderSceneShadowComparisonController(
live.RenderSceneShadow,
currentRenderSceneOracle,
message => d.Log("[UI-PROBE] " + message),
acknowledgeDirty: false)
: null;
RenderScenePViewFrameProductController? renderFrameProduct = null;
{
// Campaign V slice V6j: the world scene is composed on BOTH arms.
// Every renderer below now exists on Vulkan too; what forks is one
// pass surface, one state restorer, one GL-state reader, and the
// three renderers that stay raw GL until their own slices — sky,
// particles and the portal depth mask, which the executors already
// accept as absent.
WorldRenderDiagnostics worldRenderDiagnostics =
host.WorldRenderDiagnostics
?? new WorldRenderDiagnostics(
NullRenderGlStateReader.Instance,
d.RenderDiagnosticLog);
IRenderFrameGlState worldFrameGlState = NullRenderFrameGlState.Instance;
IWorldPassScope? worldPassScope = d.Graphics.WorldPassScope;
var worldFramebufferSource =
new SilkRetailPViewFramebufferSource(d.Window);
IWorldPassSurface worldPassSurface = new RhiWorldPassSurface(
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
host.GpuFrameLifetime,
live.ClipFrame,
worldFramebufferSource);
var worldFrameEnvironment =
new RuntimeWorldFrameEnvironmentPreparation(
d.Options,
d.WorldTime,
d.Lighting,
live.DrawDispatcher!,
live.EnvCellRenderer!,
foundation.SceneLighting!,
d.RenderRange,
skyPesFrame);
var worldRenderFrameBuilder = new WorldRenderFrameBuilder(
new RuntimeWorldFrameCameraSource(
host.CameraController,
session.LocalTeleport),
new RuntimeWorldFrameVisibilityPreparation(
live.SelectionScene,
d.ParticleVisibility,
foundation.Terrain!,
session.WorldReveal,
live.EnvCellFrustum),
new RuntimeWorldFrameSettingsPreview(
d.Settings,
content.Audio?.Engine,
host.CameraController,
d.DisplayFramePacing),
new RuntimeWorldFrameRootSource(
d.PhysicsEngine,
d.CellVisibility,
d.PlayerMode,
d.ChaseCameraInput,
d.PlayerController,
d.WorldOrigin),
worldFrameEnvironment,
new RuntimeWorldFrameAnimatedEntitySource(
d.Animations,
live.StaticAnimationScheduler,
live.EquippedChildren),
new RuntimeWorldFrameBuildingSource(
live.LandblockPipeline,
d.CellVisibility));
var terrainDrawDiagnostics = new TerrainDrawDiagnosticsController(
d.FrameDiagnosticsEnabled,
worldRenderDiagnostics,
new RuntimeFramePipelineDiagnosticFactsSource(
foundation.Terrain!,
live.LandblockPipeline,
renderFrameLivePreparation,
live.DrawDispatcher!,
session.Streaming,
live.LiveEntities,
live.WorldState),
d.RenderDiagnosticLog);
var retailPViewCells = new RetailPViewCellSource(d.CellVisibility);
var retailPViewPassExecutor = new RetailPViewPassExecutor(
worldPassSurface,
worldFrameGlState,
live.ClipFrame,
foundation.Terrain,
live.EnvCellRenderer!,
live.DrawDispatcher!,
live.SkyRenderer,
content.ParticleSystem,
live.ParticleRenderer,
live.PortalDepthMask,
d.RetailAlphaQueue,
worldRenderDiagnostics,
terrainDrawDiagnostics);
var worldSceneDiagnostics = new WorldSceneDiagnosticsController(
worldRenderDiagnostics,
new RuntimeWorldScenePViewDiagnosticSource(
d.PlayerController,
d.PhysicsEngine,
d.CellVisibility),
d.WorldSceneDebugState,
// Campaign V slice V6j: no debug-line renderer on the Vulkan arm.
// DrawAndPublish flushes it INSIDE the world phase and that
// renderer opens its own pass, which the frame's one backbuffer
// pass forbids. The collision-wireframe toggle was DevTools-only
// and the ImGui DevTools frontend is gone as of Campaign V slice
// V11, so nothing is lost — composing it would throw on the
// first wireframe frame rather than silently misdraw (plan
// §5.5.14 item 7).
null,
d.PhysicsEngine,
d.PlayerMode,
d.PlayerController,
d.DebugVmRenderFacts,
debugVmConsumerActive: false);
var worldScenePasses = new WorldScenePassExecutor(
worldPassSurface,
worldFrameGlState,
live.ClipFrame,
live.DrawDispatcher!,
live.EnvCellRenderer!,
foundation.Terrain,
terrainDrawDiagnostics,
live.SkyRenderer,
content.ParticleSystem,
live.ParticleRenderer);
renderFrameProduct =
live.RenderSceneShadow is not null
? new RenderScenePViewFrameProductController(
live.RenderSceneShadow,
currentRenderSceneOracle,
message => d.Log("[UI-PROBE] " + message),
live.DrawDispatcher!)
: null;
// After G4 the retained product is the production object source.
// The old dispatcher/selection observer is intentionally detached;
// automation still compares the independently built PView route list.
live.DrawDispatcher!.SetCurrentRenderSceneObserver(null);
live.SelectionScene.SetCurrentRenderSceneObserver(null);
worldSceneRenderer = new WorldSceneRenderer(
renderFrameResources,
renderLoginState,
d.WorldEnvironment,
worldRenderFrameBuilder,
new RuntimeWorldSceneEntitySource(live.WorldState),
live.SelectionScene,
d.RetailAlphaQueue,
d.ParticleVisibility,
new WorldScenePViewRenderer(
new RetailPViewRenderer(
currentRenderSceneOracle,
renderFrameProduct),
retailPViewPassExecutor,
retailPViewPassExecutor),
retailPViewCells,
worldScenePasses,
d.RenderRange,
worldSceneDiagnostics,
live.WorldAvailability);
// The world renderer runs INSIDE the frame's one backbuffer pass,
// which this phase opens, publishes on the scope, and closes.
worldSceneRenderer =
new AcDream.App.Rendering.Gpu.Vk.VulkanWorldScenePhase(
host.GpuFrameLifetime,
vulkanClear,
() => d.Graphics.Vulkan?.SampleCount ?? 1,
(d.Graphics as VulkanGameWindowGraphics)?.WorldPassScopeCore
?? throw new InvalidOperationException(
"The Vulkan world phase requires the Vulkan graphics handle."),
worldSceneRenderer);
}
Fault(FrameRootCompositionPoint.WorldRendererCreated);
bindings = new FrameRootRuntimeBindings();
WorldLifecycleAutomationController? lifecycleAutomation = null;
if (interaction.RetainedUi?.Screenshots is { } screenshots
&& d.Options.AutomationArtifactDirectory is { } artifactDirectory)
{
var resourceSnapshots =
new WorldLifecycleResourceSnapshotSource(
live.WorldState,
d.Animations,
live.FrameDiagnostics,
live.LiveEntities,
session.Streaming,
content.ParticleSystem,
content.ParticleSink,
live.EntityEffects,
live.Lights,
content.ScriptRunner,
foundation.MeshAdapter,
foundation.TextureCache,
live.DrawDispatcher,
d.FrameProfiler,
content.Dats,
foundation.Residency,
d.PhysicsEngine.DataCache
?? throw new InvalidOperationException(
"Lifecycle automation requires the canonical physics cache."),
currentRenderSceneOracle,
renderSceneShadowComparison,
renderFrameProduct);
lifecycleAutomation =
new WorldLifecycleAutomationController(
() => session.WorldReveal.Snapshot,
() => d.WorldEnvironment.Runtime.Ownership,
() => live.WorldTransit.Ownership,
() => session.WorldReveal.PortalMaterializationCount,
resourceSnapshots.Capture,
screenshots,
artifactDirectory,
message => d.Log("[UI-PROBE] " + message));
bindings.Adopt(
"world lifecycle automation owner",
lifecycleAutomation);
bindings.Adopt(
"world lifecycle automation binding",
interaction.LateBindings.Automation.Bind(
lifecycleAutomation));
}
else if (interaction.RetainedUi is not null)
{
// #415: without ACDREAM_AUTOMATION_ARTIFACT_DIR the deferred
// automation wrapper stayed unbound, so a probe script's
// `wait world-ready/world-visible/materialized` verbs read
// false forever and timed out even while the world revealed.
// The wait verbs need only the reveal facts — bind them always;
// checkpoint/screenshot verbs keep requiring the artifact
// directory and now report that instead of a generic timeout.
bindings.Adopt(
"world reveal facts automation binding",
interaction.LateBindings.Automation.Bind(
new WorldRevealFactsAutomationRuntime(
() => session.WorldReveal.Snapshot,
() => session.WorldReveal.PortalMaterializationCount)));
}
Fault(FrameRootCompositionPoint.LifecycleAutomationBound);
IRetainedGameplayUiFrame? retainedGameplayUi =
d.Options.RetailUi && interaction.RetainedUi is { } retained
? new RetainedGameplayUiFrame(retained.Runtime, d.Input)
: null;
IPrivateFrameScreenshot? privateScreenshot =
interaction.RetainedUi?.Screenshots is { } frameScreenshots
? new PrivateFrameScreenshot(frameScreenshots)
: null;
var privatePresentation = new PrivatePresentationRenderer(
new LocalPlayerPortalViewport(
session.LocalTeleport,
host.CameraController),
renderFrameResources,
new PrivateEntityViewportFrameGroup(
live.PaperdollPresenter,
live.CreatureAppraisalPresenter,
live.ChargenPreviewController,
live.SummaryPreviewController),
retainedGameplayUi,
// The ImGui developer-tools frontend was removed at Campaign V
// slice V11; this optional hook is unbound until a follow-up
// re-homes it onto the retained UI (IDevToolsFrameLifecycle stays
// as the seam).
devTools: null,
privateScreenshot);
var framePreparation = new RenderFramePreparationController(
renderFrameResources,
devTools: null,
renderWeatherFrame);
IRenderFramePostDiagnosticsPhase postDiagnostics =
renderSceneShadowComparison is not null
&& lifecycleAutomation is not null
? new SerialRenderFramePostDiagnosticsPhase(
renderSceneShadowComparison,
lifecycleAutomation)
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
?? NullRenderFramePostDiagnosticsPhase.Instance;
if (RenderPresentationDiagnostics.ProbeLoginFrames)
{
// Enter-world gate round (2026-08-17): the per-frame presentation
// classification probe (world/tunnel/black/void transitions). The
// tunnel fact is the controller's RAW portal-scene visibility, not
// the composed render-state source, so the probe can distinguish
// "covered black" from "tunnel scene drawn".
var loginFrameProbe = new LoginPresentationFrameProbe(
() => session.LocalTeleport.IsPortalViewportVisible,
renderLoginState,
d.Log);
postDiagnostics =
postDiagnostics is NullRenderFramePostDiagnosticsPhase
? loginFrameProbe
: new SerialRenderFramePostDiagnosticsPhase(
postDiagnostics,
loginFrameProbe);
}
var renderFrame = new RenderFrameOrchestrator(
host.GpuFrameLifetime,
// Campaign V slice V8: the Vulkan arm measures the frame bracket
// through its own timestamp scope — see VulkanFrameGpuMeasurement.
// The raw-GL adapter (FrameProfilerGpuMeasurement) was deleted at
// slice V11.
d.Graphics.Vulkan is { } vulkanGraphics
? new AcDream.App.Rendering.Gpu.Vk.VulkanFrameGpuMeasurement(
d.FrameProfiler,
vulkanGraphics.Device)
: AcDream.App.Rendering.Gpu.Vk.NullRenderFrameGpuMeasurement.Instance,
framePreparation,
worldSceneRenderer,
privatePresentation,
live.FrameDiagnostics,
postDiagnostics,
NullRenderFrameFailureRecovery.Instance);
Fault(FrameRootCompositionPoint.RenderRootCreated);
var liveFrameCoordinator = new RetailLiveFrameCoordinator(
session.LiveObjectFrame,
live.WorldState,
session.SessionHost,
session.LocalPlayerFrame,
session.LiveSpatialReconciler,
live.WorldAvailability,
live.RenderSceneShadow?.LiveProjections,
session.PlacementProjectionRetry);
var cameraFrame = new CameraFrameController(
host.CameraController,
d.InputCapture,
d.CameraInput,
session.LocalPlayerFrameRuntime,
d.ChaseCameraInput,
session.LocalPlayerFrame,
session.LiveSpatialReconciler,
new AcDream.App.Combat.CombatCameraTargetSource(
// D7 Group-C re-point (Campaign OP OP4): server bit, not
// the client-local GameplaySettings record — see
// CharacterOptionCombatSettingsSource's doc comment.
new AcDream.App.Combat.CharacterOptionCombatSettingsSource(
d.Runtime.CharacterOwner.Options),
d.Combat,
d.Selection,
live.SelectionQuery));
var updateFrame = new UpdateFrameOrchestrator(
new LiveEntityTeardownFramePhase(live.LiveEntities),
new ConsoleUpdateFrameFailureSink(),
d.UpdateClock,
new PhysicsScriptClockPublisher(content.ScriptRunner),
session.StreamingFrame,
session.GameplayInput,
liveFrameCoordinator,
new LiveEntityLivenessFramePhase(
session.Liveness,
new StopwatchClientMonotonicTimeSource()),
session.LocalTeleport,
new PlayerModeAutoEntryFramePhase(session.PlayerModeAutoEntry),
cameraFrame,
new RenderSceneUpdateCommitPhase(live.RenderSceneShadow),
live.WorldAvailability);
Fault(FrameRootCompositionPoint.UpdateRootCreated);
var bindingsLease = scope.Own(
"frame-root runtime bindings",
bindings,
static value => value.Dispose());
bindingsOwnedByScope = true;
IDisposable frameGraphPublication = d.FrameGraphs.PublishOwned(
updateFrame,
renderFrame);
var graphLease = scope.Own(
"game frame graph publication",
frameGraphPublication,
static value => value.Dispose());
Fault(FrameRootCompositionPoint.FrameGraphPublished);
var result = new FrameRootResult(
updateFrame,
renderFrame,
bindings,
frameGraphPublication,
session.SessionHost,
session.GameRuntime);
_publication.PublishFrameRoots(result);
graphLease.Transfer();
bindingsLease.Transfer();
Fault(FrameRootCompositionPoint.ResultPublished);
return result;
}
private void Fault(FrameRootCompositionPoint point) =>
_faultInjection?.Invoke(point);
}