refactor(app): compose live presentation startup
This commit is contained in:
parent
aa6ffa5176
commit
88f32dc4e2
23 changed files with 1767 additions and 626 deletions
|
|
@ -26,15 +26,14 @@ What does NOT go here:
|
||||||
|
|
||||||
## Current queue — 2026-07-22
|
## Current queue — 2026-07-22
|
||||||
|
|
||||||
- **Active structural work:** `GameWindow` decomposition. Slices 1–7 are
|
- **Active structural work:** `GameWindow` decomposition. Slices 1–7 and Slice
|
||||||
complete; Slice 8 checkpoints A–G now own native callbacks, live-session
|
8 checkpoints A–H are complete; I.1–I.6 now own the executable ordered
|
||||||
composition, world-environment state, reversible device input/raw pointer,
|
startup path through retained UI and live presentation, including the
|
||||||
physical framebuffer publication, gameplay action priority, typed commands,
|
canonical runtime/world, selection/radar, landblock publishers, and
|
||||||
the retained-root item-drop edge, and two-phase runtime settings outside the
|
portal/sky/particle resources.
|
||||||
shell.
|
|
||||||
`GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration
|
`GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration
|
||||||
handoffs and the class is 3,663 lines / 162 fields / 37 methods. Remaining
|
handoffs and the class is 2,840 raw lines. I.7–I.9 plus the remaining
|
||||||
Slice 8 resource/composition/shutdown
|
shutdown/closeout
|
||||||
cleanup is active in
|
cleanup is active in
|
||||||
[`docs/architecture/code-structure.md`](architecture/code-structure.md).
|
[`docs/architecture/code-structure.md`](architecture/code-structure.md).
|
||||||
This is the behavior-preserving prerequisite before new M4 feature bodies.
|
This is the behavior-preserving prerequisite before new M4 feature bodies.
|
||||||
|
|
|
||||||
|
|
@ -726,16 +726,17 @@ oracle, platform/host/content/settings phases, and the production world/render
|
||||||
phase. `WorldRenderCompositionPhase` owns Region/environment, the mandatory
|
phase. `WorldRenderCompositionPhase` owns Region/environment, the mandatory
|
||||||
modern-renderer foundation, immutable terrain-worker inputs, and WB/texture/
|
modern-renderer foundation, immutable terrain-worker inputs, and WB/texture/
|
||||||
sampler construction; every Phase-4 GL constructor has retryable prefix
|
sampler construction; every Phase-4 GL constructor has retryable prefix
|
||||||
ownership. `GameWindow` is 3,522 raw lines after I.5. I.6a moves interaction
|
ownership. `GameWindow` is 3,522 raw lines after I.5. I.6 moves interaction,
|
||||||
and retained-UI construction into `InteractionRetainedUiCompositionPhase`;
|
retained UI, live presentation, and landblock publication into ordered
|
||||||
typed exact-owner sources now bridge the later session, selection, radar,
|
composition phases. Typed exact-owner sources bridge later session,
|
||||||
view-plane, diagnostics, inventory, and automation owners without callbacks to
|
selection, radar, view-plane, diagnostics, inventory, hydration, and automation
|
||||||
the window. The old item-use and selection wrapper methods are deleted.
|
owners without callbacks to the window. The old item-use and selection wrapper
|
||||||
`GameWindow` is 3,246 raw lines after I.6a. I.6b–L remain active, with live
|
methods are deleted. `GameWindow` is 2,840 raw lines after I.6. I.7–L remain
|
||||||
presentation and landblock-publication composition next. The App gate passes
|
active, with session, streaming, and hydration composition next. The App gate
|
||||||
3,401 tests / 3 intentional skips and the complete Release suite passes 7,773
|
passes 3,407 tests / 3 intentional skips and the complete Release suite passes
|
||||||
tests / 5 intentional skips. The solution retains only the 17 warnings tracked
|
7,779 tests / 5 intentional skips. The clean solution build retains only the
|
||||||
by #228; the I.6a behavior, architecture, and adversarial reviews are clean.
|
17 test-project warnings tracked by #228; the I.6 behavior, architecture, and
|
||||||
|
adversarial reviews are clean.
|
||||||
|
|
||||||
### 4.4 Exit criteria
|
### 4.4 Exit criteria
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,12 +97,15 @@ runtime-target binding without replay, and one current/toon settings owner.
|
||||||
`GameWindow` is 3,663 lines / 162 fields / 37 methods at G. Checkpoint H adds
|
`GameWindow` is 3,663 lines / 162 fields / 37 methods at G. Checkpoint H adds
|
||||||
sole terrain-atlas and sky-shader roots, one retained Host/runtime lease, an
|
sole terrain-atlas and sky-shader roots, one retained Host/runtime lease, an
|
||||||
atomic update/render frame-root pair, prepared portal fallback/transfer, and
|
atomic update/render frame-root pair, prepared portal fallback/transfer, and
|
||||||
checked retryable GL leaf construction and state mutation. `GameWindow` is now
|
checked retryable GL leaf construction and state mutation. Checkpoint I.1–I.6
|
||||||
3,689 lines / 162 fields / 37 methods. Ordered composition, shutdown, canonical
|
now provide the executable ordered startup pipeline through retained UI and
|
||||||
soak snapshots, and closeout checkpoints I–L remain. Checkpoint H passes 61
|
live presentation. The canonical live runtime/world, presentation/effect
|
||||||
focused ownership tests and 3,236 App tests / 3 intentional skips; the complete
|
controllers, selection/radar, landblock publishers, and portal/sky/particle
|
||||||
Release suite passes 7,606 tests / 5 skips and all three final corrected-diff
|
resources have transactional phase ownership and exact-owner late bindings.
|
||||||
reviews are clean.
|
`GameWindow` is now 2,840 raw lines. I.7–I.9, shutdown, canonical soak
|
||||||
|
snapshots, and closeout checkpoints J–L remain. The App gate passes 3,407 tests
|
||||||
|
/ 3 intentional skips, the complete Release suite passes 7,779 tests / 5
|
||||||
|
skips, and all three I.6 corrected-diff reviews are clean.
|
||||||
|
|
||||||
This is a behavior-preserving structural program. Severe regressions still get
|
This is a behavior-preserving structural program. Severe regressions still get
|
||||||
root-cause fixes in separate commits; ordinary feature work resumes with M4
|
root-cause fixes in separate commits; ordinary feature work resumes with M4
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,15 @@ startup application, and inert late target binding. `GameWindow` is 3,663
|
||||||
lines / 162 fields / 37 methods at G. Checkpoint H adds sole terrain/sky
|
lines / 162 fields / 37 methods at G. Checkpoint H adds sole terrain/sky
|
||||||
resource roots, one retained Host/runtime lease, an atomic update/render root
|
resource roots, one retained Host/runtime lease, an atomic update/render root
|
||||||
pair, a prepared portal fallback/transfer transaction, and checked retryable GL
|
pair, a prepared portal fallback/transfer transaction, and checked retryable GL
|
||||||
leaf construction. `GameWindow` is 3,689 lines / 162 fields / 37 methods.
|
leaf construction. Checkpoint I.1–I.6 now provide the executable ordered
|
||||||
Checkpoints I–L remain active. Issue #232
|
startup pipeline through retained UI and live presentation. The canonical live
|
||||||
tracks process-residency variance in the soak without loosening its leak
|
runtime/world, presentation/effect controllers, selection/radar, landblock
|
||||||
threshold. Checkpoint H passes 61 focused ownership tests and 3,236 App tests /
|
publishers, and portal/sky/particle resources have transactional phase
|
||||||
3 skips; the complete Release suite passes 7,606 tests / 5 skips and all three
|
ownership and exact-owner late bindings. `GameWindow` is 2,840 raw lines.
|
||||||
final corrected-diff reviews are clean.
|
I.7–I.9 and checkpoints J–L remain active. Issue #232 tracks process-residency
|
||||||
|
variance in the soak without loosening its leak threshold. The App gate passes
|
||||||
|
3,407 tests / 3 skips, the complete Release suite passes 7,779 tests / 5 skips,
|
||||||
|
and all three I.6 corrected-diff reviews are clean.
|
||||||
|
|
||||||
Carried:
|
Carried:
|
||||||
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus
|
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus
|
||||||
|
|
|
||||||
|
|
@ -271,15 +271,17 @@ ledger only after the complete pipeline is live.
|
||||||
tests / 3 intentional skips; the complete Release suite passes 7,745 tests /
|
tests / 3 intentional skips; the complete Release suite passes 7,745 tests /
|
||||||
5 intentional skips. Production App builds with zero warnings/errors; the
|
5 intentional skips. Production App builds with zero warnings/errors; the
|
||||||
solution retains only the 17 warnings tracked by #228.
|
solution retains only the 17 warnings tracked by #228.
|
||||||
- I.6 (retained UI plus live-presentation composition) is active. Its detailed
|
- I.6 (retained UI plus live-presentation composition) is complete. Its detailed
|
||||||
two-commit ownership and failure-test plan is
|
two-commit ownership and failure-test plan is
|
||||||
[`2026-07-22-gamewindow-slice-8-checkpoint-i6-ui-live-presentation.md`](2026-07-22-gamewindow-slice-8-checkpoint-i6-ui-live-presentation.md).
|
[`2026-07-22-gamewindow-slice-8-checkpoint-i6-ui-live-presentation.md`](2026-07-22-gamewindow-slice-8-checkpoint-i6-ui-live-presentation.md).
|
||||||
I.6a is complete: Phase 5 now composes interaction and retained UI through
|
Phase 5 now composes interaction and retained UI through typed dependencies,
|
||||||
typed dependencies, exact-owner late bindings, and transactional rollback.
|
exact-owner late bindings, and transactional rollback. Phase 6 now composes
|
||||||
`GameWindow.cs` is 3,246 raw lines; the App gate passes 3,401 tests / 3
|
the canonical live runtime/world, presentation/effect controllers,
|
||||||
intentional skips and the complete Release suite passes 7,773 tests / 5
|
selection/radar, landblock publishers, and portal/sky/particle resources
|
||||||
intentional skips. I.6b live presentation and landblock publication is the
|
under the same ownership contract. `GameWindow.cs` is 2,840 raw lines; the
|
||||||
active cut.
|
App gate passes 3,407 tests / 3 intentional skips and the complete Release
|
||||||
|
suite passes 7,779 tests / 5 intentional skips. I.7 session, streaming, and
|
||||||
|
hydration composition is the active cut.
|
||||||
Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
|
Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
|
||||||
|
|
||||||
## 6. Automated acceptance
|
## 6. Automated acceptance
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# GameWindow Slice 8 — Checkpoint I.6 UI and Live Presentation
|
# GameWindow Slice 8 — Checkpoint I.6 UI and Live Presentation
|
||||||
|
|
||||||
**Status:** I.6a complete; I.6b active
|
**Status:** Complete; I.7 active
|
||||||
|
|
||||||
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
|
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
|
||||||
|
|
||||||
|
|
@ -195,6 +195,27 @@ The fallback slot and final owner never co-own the tunnel.
|
||||||
- App Release build/tests, full Release build/tests, divergence/source audit;
|
- App Release build/tests, full Release build/tests, divergence/source audit;
|
||||||
- three independent self-review passes as in I.6a.
|
- three independent self-review passes as in I.6a.
|
||||||
|
|
||||||
|
### 3.5 I.6b completion evidence
|
||||||
|
|
||||||
|
I.6b moved the complete Phase-6 body into
|
||||||
|
`LivePresentationCompositionPhase`. The phase now owns the canonical live
|
||||||
|
runtime/world identity, live presentation and effect controllers, WB draw and
|
||||||
|
landblock publication owners, selection/radar authorities, optional paperdoll,
|
||||||
|
and portal/sky/particle resources. Named exact-owner leases replace the former
|
||||||
|
anonymous Phase-6 subscriptions. A deferred landblock-loaded sink preserves
|
||||||
|
the Phase-7 hydration edge without capturing a future window field.
|
||||||
|
|
||||||
|
Construction uses transactional acquisition ownership throughout, including
|
||||||
|
portal-fallback and sky-shader lifetime rollback. The focused binding,
|
||||||
|
rollback, retry, stale-release, and source-boundary tests pass. The App Release
|
||||||
|
gate passes 3,407 tests / 3 intentional skips, the complete Release suite
|
||||||
|
passes 7,779 tests / 5 intentional skips, and the clean solution build retains
|
||||||
|
only the 17 test-project warnings tracked by #228. `GameWindow.cs` is 2,840
|
||||||
|
raw lines after I.6b.
|
||||||
|
Behavior/fidelity, architecture/ownership, and adversarial failure reviews
|
||||||
|
found no remaining actionable issue. I.7 session, streaming, and hydration
|
||||||
|
composition is the next active cut.
|
||||||
|
|
||||||
## 4. Production cutover rules
|
## 4. Production cutover rules
|
||||||
|
|
||||||
- Each subcommit leaves `OnLoad` calling the new phase and leaves later phases
|
- Each subcommit leaves `OnLoad` calling the new phase and leaves later phases
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,31 @@ internal sealed class AnimationHookRegistrationSet : IDisposable,
|
||||||
_entries.Add(new Entry(sink));
|
_entries.Add(new Entry(sink));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable RegisterOwned(IAnimationHookSink sink)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(sink);
|
||||||
|
ObjectDisposedException.ThrowIf(_closed, this);
|
||||||
|
if (_entries.Any(entry =>
|
||||||
|
!entry.Removed && ReferenceEquals(entry.Sink, sink)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The animation-hook sink is already registered and cannot be adopted twice.");
|
||||||
|
}
|
||||||
|
_register(sink);
|
||||||
|
_entries.Add(new Entry(sink));
|
||||||
|
return new Binding(this, sink);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnregisterOwned(IAnimationHookSink sink)
|
||||||
|
{
|
||||||
|
Entry? entry = _entries.LastOrDefault(candidate =>
|
||||||
|
!candidate.Removed && ReferenceEquals(candidate.Sink, sink));
|
||||||
|
if (entry is null)
|
||||||
|
return;
|
||||||
|
_unregister(entry.Sink);
|
||||||
|
entry.Removed = true;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_closed = true;
|
_closed = true;
|
||||||
|
|
@ -102,4 +127,25 @@ internal sealed class AnimationHookRegistrationSet : IDisposable,
|
||||||
"Animation-hook registration cleanup remains incomplete.",
|
"Animation-hook registration cleanup remains incomplete.",
|
||||||
failures);
|
failures);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private AnimationHookRegistrationSet? _owner;
|
||||||
|
private readonly IAnimationHookSink _sink;
|
||||||
|
|
||||||
|
public Binding(AnimationHookRegistrationSet owner, IAnimationHookSink sink)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_sink = sink;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
AnimationHookRegistrationSet? owner = _owner;
|
||||||
|
if (owner is null)
|
||||||
|
return;
|
||||||
|
owner.UnregisterOwned(_sink);
|
||||||
|
_owner = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
938
src/AcDream.App/Composition/LivePresentationComposition.cs
Normal file
938
src/AcDream.App/Composition/LivePresentationComposition.cs
Normal file
|
|
@ -0,0 +1,938 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Diagnostics;
|
||||||
|
using AcDream.App.Input;
|
||||||
|
using AcDream.App.Interaction;
|
||||||
|
using AcDream.App.Physics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Selection;
|
||||||
|
using AcDream.App.Rendering.Sky;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Settings;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
using AcDream.App.UI;
|
||||||
|
using AcDream.App.UI.Layout;
|
||||||
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.Audio;
|
||||||
|
using AcDream.Core.Items;
|
||||||
|
using AcDream.Core.Lighting;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Core.Plugins;
|
||||||
|
using AcDream.Core.Rendering;
|
||||||
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.Core.Vfx;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using Silk.NET.OpenGL;
|
||||||
|
using Silk.NET.Windowing;
|
||||||
|
|
||||||
|
namespace AcDream.App.Composition;
|
||||||
|
|
||||||
|
internal sealed record LivePresentationDependencies(
|
||||||
|
RuntimeOptions Options,
|
||||||
|
GL Gl,
|
||||||
|
IWindow Window,
|
||||||
|
object DatLock,
|
||||||
|
RuntimeSettingsController Settings,
|
||||||
|
HostQuiescenceGate HostQuiescence,
|
||||||
|
PhysicsEngine PhysicsEngine,
|
||||||
|
PhysicsDataCache PhysicsDataCache,
|
||||||
|
WorldGameState WorldGameState,
|
||||||
|
WorldEvents WorldEvents,
|
||||||
|
SelectionState Selection,
|
||||||
|
ClientObjectTable Objects,
|
||||||
|
LiveEntityRuntimeSlot RuntimeSlot,
|
||||||
|
DeferredLiveEntityMotionRuntimeBindings MotionBindings,
|
||||||
|
DeferredEntityEffectAdvanceSource EffectAdvance,
|
||||||
|
EntityEffectPoseRegistry EffectPoses,
|
||||||
|
RemotePhysicsUpdater RemotePhysicsUpdater,
|
||||||
|
LocalPlayerShadowState LocalPlayerShadow,
|
||||||
|
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> AnimatedEntities,
|
||||||
|
AnimationPresentationDiagnostics AnimationDiagnostics,
|
||||||
|
EntityClassificationCache ClassificationCache,
|
||||||
|
TranslucencyFadeManager TranslucencyFades,
|
||||||
|
RetailAlphaQueue RetailAlphaQueue,
|
||||||
|
CellVisibility CellVisibility,
|
||||||
|
LiveWorldOriginState WorldOrigin,
|
||||||
|
LocalPlayerIdentityState PlayerIdentity,
|
||||||
|
LocalPlayerControllerSlot PlayerController,
|
||||||
|
PointerPositionState PointerPosition,
|
||||||
|
PlayerApproachCompletionState PlayerApproachCompletions,
|
||||||
|
GameRenderResourceLifetime RenderResourceLifetime,
|
||||||
|
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
||||||
|
AnimationHookRouter HookRouter,
|
||||||
|
IRenderFrameDiagnosticLog RenderDiagnosticLog,
|
||||||
|
WorldTimeService WorldTime,
|
||||||
|
DeferredCanonicalWorldEntityCountSource? DevWorldEntities,
|
||||||
|
DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics,
|
||||||
|
DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics,
|
||||||
|
Action<string>? Toast);
|
||||||
|
|
||||||
|
internal sealed record LivePresentationResult(
|
||||||
|
DeferredLiveEntityRuntimeComponentLifecycle ComponentLifecycle,
|
||||||
|
LiveEntityMotionRuntimeController MotionRuntime,
|
||||||
|
DeferredLiveEntityParentAcceptance ParentAcceptance,
|
||||||
|
EntitySpawnAdapter EntitySpawnAdapter,
|
||||||
|
EntityScriptActivator EntityScriptActivator,
|
||||||
|
RetailStaticAnimatingObjectScheduler StaticAnimationScheduler,
|
||||||
|
GpuWorldState WorldState,
|
||||||
|
LiveEntityRuntime LiveEntities,
|
||||||
|
ProjectileController ProjectileController,
|
||||||
|
LiveEntityProjectionWithdrawalController ProjectionWithdrawal,
|
||||||
|
LiveEntityLightController Lights,
|
||||||
|
LiveEntityAnimationScheduler AnimationScheduler,
|
||||||
|
LiveEntityAnimationPresenter AnimationPresenter,
|
||||||
|
EquippedChildRenderController EquippedChildren,
|
||||||
|
EntityEffectController EntityEffects,
|
||||||
|
LiveEntityPresentationController Presentation,
|
||||||
|
RemoteTeleportController RemoteTeleport,
|
||||||
|
WbDrawDispatcher DrawDispatcher,
|
||||||
|
RetailSelectionScene SelectionScene,
|
||||||
|
WorldSelectionQuery SelectionQuery,
|
||||||
|
SelectionInteractionController SelectionInteractions,
|
||||||
|
RetainedUiGameplayBinding? RetainedGameplay,
|
||||||
|
PaperdollViewportRenderer? PaperdollRenderer,
|
||||||
|
PaperdollFramePresenter? PaperdollPresenter,
|
||||||
|
WbFrustum EnvCellFrustum,
|
||||||
|
EnvCellRenderer EnvCellRenderer,
|
||||||
|
LandblockPresentationPipeline LandblockPipeline,
|
||||||
|
ClipFrame ClipFrame,
|
||||||
|
PortalDepthMaskRenderer PortalDepthMask,
|
||||||
|
SkyRenderer SkyRenderer,
|
||||||
|
ParticleRenderer ParticleRenderer,
|
||||||
|
RenderFrameDiagnosticsController FrameDiagnostics,
|
||||||
|
LivePresentationRuntimeBindings RuntimeBindings,
|
||||||
|
DeferredLiveEntityLandblockLoadedSink LandblockLoaded);
|
||||||
|
|
||||||
|
internal interface IGameWindowLivePresentationPublication
|
||||||
|
{
|
||||||
|
void PublishLivePresentation(LivePresentationResult result);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum LivePresentationCompositionPoint
|
||||||
|
{
|
||||||
|
CanonicalRuntimeCreated,
|
||||||
|
CanonicalRuntimeBound,
|
||||||
|
MotionRuntimeBound,
|
||||||
|
ProjectionVisibilityBound,
|
||||||
|
CorePresentationCreated,
|
||||||
|
EffectRoutingBound,
|
||||||
|
SelectionAndRadarBound,
|
||||||
|
RetainedGameplayBound,
|
||||||
|
PaperdollCreated,
|
||||||
|
EnvironmentCellsCreated,
|
||||||
|
LandblockPipelineCreated,
|
||||||
|
PortalResourcesCreated,
|
||||||
|
SkyAndParticlesCreated,
|
||||||
|
DiagnosticsBound,
|
||||||
|
ResultPublished,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class LivePresentationCompositionPhase
|
||||||
|
: ILivePresentationCompositionPhase<
|
||||||
|
HostInputCameraResult,
|
||||||
|
ContentEffectsAudioResult,
|
||||||
|
WorldRenderResult,
|
||||||
|
InteractionRetainedUiResult,
|
||||||
|
LivePresentationResult>
|
||||||
|
{
|
||||||
|
private readonly LivePresentationDependencies _dependencies;
|
||||||
|
private readonly IGameWindowLivePresentationPublication _publication;
|
||||||
|
private readonly Action<LivePresentationCompositionPoint>? _faultInjection;
|
||||||
|
|
||||||
|
public LivePresentationCompositionPhase(
|
||||||
|
LivePresentationDependencies dependencies,
|
||||||
|
IGameWindowLivePresentationPublication publication,
|
||||||
|
Action<LivePresentationCompositionPoint>? faultInjection = null)
|
||||||
|
{
|
||||||
|
_dependencies = dependencies
|
||||||
|
?? throw new ArgumentNullException(nameof(dependencies));
|
||||||
|
_publication = publication
|
||||||
|
?? throw new ArgumentNullException(nameof(publication));
|
||||||
|
_faultInjection = faultInjection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LivePresentationResult Compose(
|
||||||
|
HostInputCameraResult host,
|
||||||
|
ContentEffectsAudioResult content,
|
||||||
|
WorldRenderResult world,
|
||||||
|
InteractionRetainedUiResult interaction)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(host);
|
||||||
|
ArgumentNullException.ThrowIfNull(content);
|
||||||
|
ArgumentNullException.ThrowIfNull(world);
|
||||||
|
ArgumentNullException.ThrowIfNull(interaction);
|
||||||
|
return ComposeCore(host, content, world, interaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LivePresentationResult ComposeCore(
|
||||||
|
HostInputCameraResult host,
|
||||||
|
ContentEffectsAudioResult content,
|
||||||
|
WorldRenderResult world,
|
||||||
|
InteractionRetainedUiResult interaction)
|
||||||
|
{
|
||||||
|
LivePresentationDependencies d = _dependencies;
|
||||||
|
WorldRenderFoundation foundation = world.Foundation;
|
||||||
|
var scope = new CompositionAcquisitionScope();
|
||||||
|
LivePresentationRuntimeBindings? bindings = null;
|
||||||
|
bool bindingsOwnedByScope = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var componentLifecycle =
|
||||||
|
new DeferredLiveEntityRuntimeComponentLifecycle();
|
||||||
|
var wbSpawnAdapter = new LandblockSpawnAdapter(foundation.MeshAdapter);
|
||||||
|
|
||||||
|
AnimationSequencer SequencerFactory(WorldEntity entity)
|
||||||
|
{
|
||||||
|
Setup? setup = content.Dats.Get<Setup>(entity.SourceGfxObjOrSetupId);
|
||||||
|
if (setup is not null)
|
||||||
|
{
|
||||||
|
uint motionTableId = (uint)setup.DefaultMotionTable;
|
||||||
|
if (motionTableId != 0
|
||||||
|
&& content.Dats.Get<MotionTable>(motionTableId) is { } motionTable)
|
||||||
|
{
|
||||||
|
return new AnimationSequencer(
|
||||||
|
setup,
|
||||||
|
motionTable,
|
||||||
|
content.AnimationLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AnimationSequencer(
|
||||||
|
setup,
|
||||||
|
new MotionTable(),
|
||||||
|
content.AnimationLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AnimationSequencer(
|
||||||
|
new Setup(),
|
||||||
|
new MotionTable(),
|
||||||
|
NullAnimationLoader.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entitySpawnAdapter = new EntitySpawnAdapter(
|
||||||
|
foundation.TextureCache,
|
||||||
|
SequencerFactory,
|
||||||
|
foundation.MeshAdapter);
|
||||||
|
EntityEffectController? entityEffects = null;
|
||||||
|
LiveEntityRuntime? liveEntities = null;
|
||||||
|
var staticRootCommitter = new StaticLiveRootCommitter(
|
||||||
|
d.RuntimeSlot,
|
||||||
|
d.PhysicsEngine.ShadowObjects,
|
||||||
|
d.WorldOrigin,
|
||||||
|
d.EffectPoses);
|
||||||
|
var staticResidency = new LiveStaticAnimationResidency(d.RuntimeSlot);
|
||||||
|
var staticAnimationScheduler =
|
||||||
|
new RetailStaticAnimatingObjectScheduler(
|
||||||
|
content.AnimationLoader,
|
||||||
|
content.AnimationHookFrames.Capture,
|
||||||
|
d.EffectPoses.Publish,
|
||||||
|
staticResidency.IsResident,
|
||||||
|
(entity, body) => _ = staticRootCommitter.Commit(entity, body),
|
||||||
|
staticResidency.ProjectionVersion);
|
||||||
|
|
||||||
|
ScriptActivationInfo? ResolveActivation(WorldEntity entity)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup? setup = content.Dats.Get<Setup>(
|
||||||
|
entity.SourceGfxObjOrSetupId);
|
||||||
|
if (setup is null)
|
||||||
|
return null;
|
||||||
|
uint scriptId = setup.DefaultScript.DataId;
|
||||||
|
if (entity.IndexedPartTransforms.Count == 0)
|
||||||
|
{
|
||||||
|
var indexed = IndexedSetupPartPoseBuilder.Build(setup, entity);
|
||||||
|
entity.SetIndexedPartPoses(indexed.Poses, indexed.Available);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool usesStaticAnimationWorkset = entity.ServerGuid == 0
|
||||||
|
|| (liveEntities?.TryGetRecord(
|
||||||
|
entity.ServerGuid,
|
||||||
|
out LiveEntityRecord liveRecord) == true
|
||||||
|
&& (liveRecord.FinalPhysicsState
|
||||||
|
& PhysicsStateFlags.Static) != 0);
|
||||||
|
return new ScriptActivationInfo(
|
||||||
|
scriptId,
|
||||||
|
entity.IndexedPartTransforms,
|
||||||
|
EntityEffectProfile.CreateDatStatic(setup),
|
||||||
|
entity.IndexedPartAvailable,
|
||||||
|
setup,
|
||||||
|
(uint)setup.DefaultAnimation,
|
||||||
|
usesStaticAnimationWorkset);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var entityScriptActivator = new EntityScriptActivator(
|
||||||
|
content.ScriptRunner,
|
||||||
|
content.ParticleSink,
|
||||||
|
d.EffectPoses,
|
||||||
|
ResolveActivation,
|
||||||
|
(ownerId, entity, profile) =>
|
||||||
|
entityEffects?.OnDatStaticEntityReady(ownerId, entity, profile),
|
||||||
|
ownerId =>
|
||||||
|
{
|
||||||
|
entityEffects?.OnDatStaticEntityRemoved(ownerId);
|
||||||
|
content.LightingSink.UnregisterOwner(ownerId);
|
||||||
|
d.TranslucencyFades.ClearEntity(ownerId);
|
||||||
|
},
|
||||||
|
(entity, info) => staticAnimationScheduler.Register(entity, info),
|
||||||
|
staticAnimationScheduler.Unregister,
|
||||||
|
(entity, info) => staticAnimationScheduler.Rebind(entity, info));
|
||||||
|
|
||||||
|
var worldState = new GpuWorldState(
|
||||||
|
wbSpawnAdapter,
|
||||||
|
d.ClassificationCache.InvalidateLandblock,
|
||||||
|
entityScriptActivator);
|
||||||
|
bindings = new LivePresentationRuntimeBindings();
|
||||||
|
if (d.DevWorldEntities is { } devWorldEntities)
|
||||||
|
{
|
||||||
|
bindings.Adopt(
|
||||||
|
"developer world-entity source",
|
||||||
|
devWorldEntities.BindOwned(worldState));
|
||||||
|
}
|
||||||
|
liveEntities = new LiveEntityRuntime(
|
||||||
|
worldState,
|
||||||
|
new CompositeLiveEntityResourceLifecycle(
|
||||||
|
new(
|
||||||
|
entity => _ = entitySpawnAdapter.OnCreate(entity),
|
||||||
|
entity => _ = entitySpawnAdapter.OnRemove(entity)),
|
||||||
|
new(
|
||||||
|
entityScriptActivator.OnCreate,
|
||||||
|
entityScriptActivator.OnRemove)),
|
||||||
|
componentLifecycle);
|
||||||
|
var liveRuntimeLease = scope.Own(
|
||||||
|
"canonical live-entity runtime",
|
||||||
|
liveEntities,
|
||||||
|
static runtime => runtime.Clear());
|
||||||
|
Fault(LivePresentationCompositionPoint.CanonicalRuntimeCreated);
|
||||||
|
|
||||||
|
bindings.Adopt(
|
||||||
|
"canonical live-runtime slot",
|
||||||
|
d.RuntimeSlot.BindOwned(liveEntities));
|
||||||
|
Fault(LivePresentationCompositionPoint.CanonicalRuntimeBound);
|
||||||
|
|
||||||
|
var selectionInteractionSource =
|
||||||
|
new DeferredSelectionInteractionSource();
|
||||||
|
var motionRuntime = new LiveEntityMotionRuntimeController(
|
||||||
|
liveEntities,
|
||||||
|
d.PhysicsDataCache,
|
||||||
|
() => selectionInteractionSource.Current,
|
||||||
|
d.Selection,
|
||||||
|
d.WorldOrigin);
|
||||||
|
bindings.Adopt(
|
||||||
|
"live motion runtime",
|
||||||
|
d.MotionBindings.BindOwned(motionRuntime));
|
||||||
|
Fault(LivePresentationCompositionPoint.MotionRuntimeBound);
|
||||||
|
|
||||||
|
Action<LiveEntityRecord, bool> wbVisibility = (record, visible) =>
|
||||||
|
{
|
||||||
|
if (record.WorldEntity is { } entity)
|
||||||
|
entitySpawnAdapter.SetPresentationResident(entity, visible);
|
||||||
|
};
|
||||||
|
bindings.BindProjectionVisibility(
|
||||||
|
liveEntities,
|
||||||
|
wbVisibility,
|
||||||
|
"WB projection visibility");
|
||||||
|
Action<LiveEntityRecord, bool> particleVisibility = (record, visible) =>
|
||||||
|
{
|
||||||
|
if (record.WorldEntity is { } entity)
|
||||||
|
content.ParticleSink.SetEntityPresentationVisible(entity.Id, visible);
|
||||||
|
};
|
||||||
|
bindings.BindProjectionVisibility(
|
||||||
|
liveEntities,
|
||||||
|
particleVisibility,
|
||||||
|
"particle projection visibility");
|
||||||
|
Fault(LivePresentationCompositionPoint.ProjectionVisibilityBound);
|
||||||
|
|
||||||
|
var projectileController = new ProjectileController(
|
||||||
|
liveEntities,
|
||||||
|
d.PhysicsEngine,
|
||||||
|
new DatProjectileSetupResolver(content.Dats, d.DatLock),
|
||||||
|
new EntityRootPosePublisher(d.EffectPoses),
|
||||||
|
d.WorldOrigin)
|
||||||
|
{
|
||||||
|
DiagnosticSink = message =>
|
||||||
|
Console.Error.WriteLine($"projectile: {message}"),
|
||||||
|
};
|
||||||
|
var projectionWithdrawal =
|
||||||
|
new LiveEntityProjectionWithdrawalController(
|
||||||
|
liveEntities,
|
||||||
|
projectileController,
|
||||||
|
d.WorldGameState,
|
||||||
|
d.WorldEvents,
|
||||||
|
d.PhysicsEngine.ShadowObjects,
|
||||||
|
d.EffectPoses,
|
||||||
|
d.LocalPlayerShadow);
|
||||||
|
var lightsLease = scope.Acquire(
|
||||||
|
"live-entity lights",
|
||||||
|
() => new LiveEntityLightController(
|
||||||
|
liveEntities,
|
||||||
|
d.EffectPoses,
|
||||||
|
content.LightingSink,
|
||||||
|
setupId => content.Dats.Get<Setup>(setupId)),
|
||||||
|
static value => value.Dispose());
|
||||||
|
var ordinaryPhysicsUpdater = new LiveEntityOrdinaryPhysicsUpdater(
|
||||||
|
d.PhysicsEngine,
|
||||||
|
d.MotionBindings.GetSetupCylinder);
|
||||||
|
var animationScheduler = new LiveEntityAnimationScheduler(
|
||||||
|
liveEntities,
|
||||||
|
d.PlayerIdentity,
|
||||||
|
d.RemotePhysicsUpdater,
|
||||||
|
ordinaryPhysicsUpdater,
|
||||||
|
projectileController,
|
||||||
|
new EntityRootPosePublisher(d.EffectPoses),
|
||||||
|
new AnimationHookCaptureSink(content.AnimationHookFrames));
|
||||||
|
var animationPresenter = new LiveEntityAnimationPresenter(
|
||||||
|
liveEntities,
|
||||||
|
staticAnimationScheduler,
|
||||||
|
d.EffectPoses,
|
||||||
|
new LiveAnimationPresentationContext(
|
||||||
|
liveEntities,
|
||||||
|
d.PlayerIdentity,
|
||||||
|
d.PlayerController),
|
||||||
|
d.AnimationDiagnostics,
|
||||||
|
d.Options.HidePartIndex);
|
||||||
|
var parentAcceptance = new DeferredLiveEntityParentAcceptance();
|
||||||
|
var equippedLease = scope.Acquire(
|
||||||
|
"equipped-child renderer",
|
||||||
|
() => new EquippedChildRenderController(
|
||||||
|
content.Dats,
|
||||||
|
d.DatLock,
|
||||||
|
d.Objects,
|
||||||
|
liveEntities,
|
||||||
|
d.EffectPoses,
|
||||||
|
parentAcceptance.TryAccept,
|
||||||
|
(childRecord, positionVersion, projectionVersion) =>
|
||||||
|
projectionWithdrawal.WithdrawExact(
|
||||||
|
childRecord,
|
||||||
|
positionVersion,
|
||||||
|
projectionVersion,
|
||||||
|
d.PlayerIdentity.ServerGuid)),
|
||||||
|
static value => value.Dispose());
|
||||||
|
Fault(LivePresentationCompositionPoint.CorePresentationCreated);
|
||||||
|
|
||||||
|
var tableResolver = new PhysicsScriptTableResolver(
|
||||||
|
id => content.Dats.Get<PhysicsScriptTable>(id));
|
||||||
|
entityEffects = new EntityEffectController(
|
||||||
|
liveEntities,
|
||||||
|
content.ScriptRunner,
|
||||||
|
tableResolver,
|
||||||
|
d.EffectPoses,
|
||||||
|
(parentLocalId, partIndex) =>
|
||||||
|
equippedLease.Resource.FindChildLocalIdAtPart(
|
||||||
|
parentLocalId,
|
||||||
|
partIndex),
|
||||||
|
equippedLease.Resource.FindParentLocalId,
|
||||||
|
ownerId => content.Audio?.EntitySoundTables.Remove(ownerId),
|
||||||
|
(ownerId, soundTableDid) =>
|
||||||
|
{
|
||||||
|
content.Audio?.EntitySoundTables.Remove(ownerId);
|
||||||
|
if (soundTableDid is { } did)
|
||||||
|
content.Audio?.EntitySoundTables.Set(ownerId, did);
|
||||||
|
});
|
||||||
|
bindings.Adopt(
|
||||||
|
"entity-effect advance",
|
||||||
|
d.EffectAdvance.BindOwned(entityEffects));
|
||||||
|
entityEffects.DiagnosticSink = message =>
|
||||||
|
Console.Error.WriteLine($"vfx: {message}");
|
||||||
|
var partArrayLifecycle = new LiveEntityPartArrayLifecycle(
|
||||||
|
d.AnimatedEntities);
|
||||||
|
var presentationLease = scope.Acquire(
|
||||||
|
"live-entity presentation",
|
||||||
|
() => new LiveEntityPresentationController(
|
||||||
|
liveEntities,
|
||||||
|
d.PhysicsEngine.ShadowObjects,
|
||||||
|
entityEffects.PlayTypedFromHiddenTransition,
|
||||||
|
equippedLease.Resource.SetDirectChildrenNoDraw,
|
||||||
|
d.MotionBindings.ClearTargetForHiddenEntity,
|
||||||
|
d.WorldOrigin.GetCenter,
|
||||||
|
partArrayLifecycle.HandleEnterWorld),
|
||||||
|
static value => value.Dispose());
|
||||||
|
var remoteShadowPlacement = new RemoteShadowPlacementSynchronizer(
|
||||||
|
d.RemotePhysicsUpdater,
|
||||||
|
d.WorldOrigin);
|
||||||
|
var remoteTeleportPresentation =
|
||||||
|
new RemoteTeleportPlacementPresentation(presentationLease.Resource);
|
||||||
|
var remoteTeleportLease = scope.Acquire(
|
||||||
|
"remote teleport",
|
||||||
|
() => new RemoteTeleportController(
|
||||||
|
d.PhysicsEngine,
|
||||||
|
liveEntities,
|
||||||
|
d.MotionBindings.GetSetupCylinder,
|
||||||
|
d.WorldOrigin.CellLocalForSeed,
|
||||||
|
remoteShadowPlacement.Sync,
|
||||||
|
remoteTeleportPresentation.Complete,
|
||||||
|
remoteTeleportPresentation.Begin),
|
||||||
|
static value => value.Dispose());
|
||||||
|
bindings.BindProjectionPoseReady(
|
||||||
|
equippedLease.Resource,
|
||||||
|
lightsLease.Resource.OnAttachedPoseReady);
|
||||||
|
bindings.Adopt(
|
||||||
|
"entity-effect animation hooks",
|
||||||
|
content.HookRegistrations.RegisterOwned(entityEffects));
|
||||||
|
Fault(LivePresentationCompositionPoint.EffectRoutingBound);
|
||||||
|
|
||||||
|
return CompletePresentation(
|
||||||
|
host,
|
||||||
|
content,
|
||||||
|
world,
|
||||||
|
interaction,
|
||||||
|
componentLifecycle,
|
||||||
|
motionRuntime,
|
||||||
|
parentAcceptance,
|
||||||
|
entitySpawnAdapter,
|
||||||
|
entityScriptActivator,
|
||||||
|
staticAnimationScheduler,
|
||||||
|
worldState,
|
||||||
|
liveEntities,
|
||||||
|
projectileController,
|
||||||
|
projectionWithdrawal,
|
||||||
|
lightsLease,
|
||||||
|
animationScheduler,
|
||||||
|
animationPresenter,
|
||||||
|
equippedLease,
|
||||||
|
entityEffects,
|
||||||
|
presentationLease,
|
||||||
|
remoteTeleportLease,
|
||||||
|
selectionInteractionSource,
|
||||||
|
bindings,
|
||||||
|
scope,
|
||||||
|
ref bindingsOwnedByScope,
|
||||||
|
liveRuntimeLease);
|
||||||
|
}
|
||||||
|
catch (Exception failure)
|
||||||
|
{
|
||||||
|
if (bindings is not null && !bindingsOwnedByScope)
|
||||||
|
{
|
||||||
|
scope.Own(
|
||||||
|
"live-presentation runtime bindings",
|
||||||
|
bindings,
|
||||||
|
static value => value.Dispose());
|
||||||
|
bindingsOwnedByScope = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.RollbackAndThrow(failure);
|
||||||
|
throw new System.Diagnostics.UnreachableException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LivePresentationResult CompletePresentation(
|
||||||
|
HostInputCameraResult host,
|
||||||
|
ContentEffectsAudioResult content,
|
||||||
|
WorldRenderResult world,
|
||||||
|
InteractionRetainedUiResult interaction,
|
||||||
|
DeferredLiveEntityRuntimeComponentLifecycle componentLifecycle,
|
||||||
|
LiveEntityMotionRuntimeController motionRuntime,
|
||||||
|
DeferredLiveEntityParentAcceptance parentAcceptance,
|
||||||
|
EntitySpawnAdapter entitySpawnAdapter,
|
||||||
|
EntityScriptActivator entityScriptActivator,
|
||||||
|
RetailStaticAnimatingObjectScheduler staticAnimationScheduler,
|
||||||
|
GpuWorldState worldState,
|
||||||
|
LiveEntityRuntime liveEntities,
|
||||||
|
ProjectileController projectileController,
|
||||||
|
LiveEntityProjectionWithdrawalController projectionWithdrawal,
|
||||||
|
CompositionAcquisitionScope.CompositionAcquisitionLease<LiveEntityLightController> lightsLease,
|
||||||
|
LiveEntityAnimationScheduler animationScheduler,
|
||||||
|
LiveEntityAnimationPresenter animationPresenter,
|
||||||
|
CompositionAcquisitionScope.CompositionAcquisitionLease<EquippedChildRenderController> equippedLease,
|
||||||
|
EntityEffectController entityEffects,
|
||||||
|
CompositionAcquisitionScope.CompositionAcquisitionLease<LiveEntityPresentationController> presentationLease,
|
||||||
|
CompositionAcquisitionScope.CompositionAcquisitionLease<RemoteTeleportController> remoteTeleportLease,
|
||||||
|
DeferredSelectionInteractionSource selectionInteractionSource,
|
||||||
|
LivePresentationRuntimeBindings bindings,
|
||||||
|
CompositionAcquisitionScope scope,
|
||||||
|
ref bool bindingsOwnedByScope,
|
||||||
|
CompositionAcquisitionScope.CompositionAcquisitionLease<LiveEntityRuntime> liveRuntimeLease)
|
||||||
|
{
|
||||||
|
LivePresentationDependencies d = _dependencies;
|
||||||
|
WorldRenderFoundation foundation = world.Foundation;
|
||||||
|
|
||||||
|
var selectionScene = new RetailSelectionScene(
|
||||||
|
new RetailSelectionGeometryCache(content.Dats, d.DatLock));
|
||||||
|
var dispatcherLease = scope.Acquire(
|
||||||
|
"WB draw dispatcher",
|
||||||
|
() => new WbDrawDispatcher(
|
||||||
|
d.Gl,
|
||||||
|
foundation.MeshShader,
|
||||||
|
foundation.TextureCache,
|
||||||
|
foundation.MeshAdapter,
|
||||||
|
entitySpawnAdapter,
|
||||||
|
foundation.Bindless,
|
||||||
|
d.ClassificationCache,
|
||||||
|
d.TranslucencyFades,
|
||||||
|
selectionScene,
|
||||||
|
d.RetailAlphaQueue),
|
||||||
|
static value => value.Dispose());
|
||||||
|
var selectionQuery = new WorldSelectionQuery(
|
||||||
|
liveEntities,
|
||||||
|
d.Objects,
|
||||||
|
selectionScene,
|
||||||
|
() => d.PlayerIdentity.ServerGuid,
|
||||||
|
interaction.LateBindings.SelectionCamera.Snapshot,
|
||||||
|
() => new Vector2(
|
||||||
|
d.PointerPosition.X,
|
||||||
|
d.PointerPosition.Y),
|
||||||
|
() => d.PlayerController.Controller is { } player
|
||||||
|
? new PlayerInteractionPose(player.CellId, player.Position)
|
||||||
|
: null,
|
||||||
|
d.MotionBindings.GetSetupCylinder,
|
||||||
|
setupId =>
|
||||||
|
{
|
||||||
|
lock (d.DatLock)
|
||||||
|
{
|
||||||
|
if (!content.Dats.TryGet<Setup>(setupId, out Setup? setup)
|
||||||
|
|| setup.SelectionSphere is not { } sphere)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (sphere.Origin, sphere.Radius);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var radarSnapshotProvider = new RadarSnapshotProvider(
|
||||||
|
d.Objects,
|
||||||
|
() => liveEntities.WorldEntities,
|
||||||
|
() => liveEntities.Snapshots,
|
||||||
|
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||||
|
playerYawRadians: () => d.PlayerController.Controller?.Yaw ?? 0f,
|
||||||
|
playerCellId: () => d.PlayerController.Controller?.CellId ?? 0u,
|
||||||
|
selectedGuid: () => d.Selection.SelectedObjectId,
|
||||||
|
coordinatesOnRadar: () => d.Settings.Gameplay.CoordinatesOnRadar,
|
||||||
|
uiLocked: () => d.Settings.Gameplay.LockUI,
|
||||||
|
playerEntities: () => liveEntities.MaterializedWorldEntities,
|
||||||
|
spatialQuery: () => worldState);
|
||||||
|
bindings.Adopt(
|
||||||
|
"radar snapshot",
|
||||||
|
interaction.LateBindings.Radar.Bind(radarSnapshotProvider));
|
||||||
|
var selectionInteractions = new SelectionInteractionController(
|
||||||
|
d.Selection,
|
||||||
|
selectionQuery,
|
||||||
|
interaction.ItemInteraction,
|
||||||
|
new WorldSessionSelectionInteractionTransport(
|
||||||
|
() => interaction.LateBindings.Session.CurrentSession),
|
||||||
|
new PlayerInteractionMovementSink(
|
||||||
|
() => d.PlayerController.Controller,
|
||||||
|
d.PlayerApproachCompletions),
|
||||||
|
d.Toast,
|
||||||
|
d.PlayerApproachCompletions);
|
||||||
|
selectionInteractionSource.Bind(selectionInteractions);
|
||||||
|
bindings.Adopt(
|
||||||
|
"world selection",
|
||||||
|
interaction.LateBindings.Selection.Bind(
|
||||||
|
selectionQuery,
|
||||||
|
selectionInteractions));
|
||||||
|
Fault(LivePresentationCompositionPoint.SelectionAndRadarBound);
|
||||||
|
|
||||||
|
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||||
|
RetainedUiGameplayBinding>? retainedGameplayLease = null;
|
||||||
|
if (interaction.RetainedUi is { } retainedUi)
|
||||||
|
{
|
||||||
|
retainedGameplayLease = scope.Acquire(
|
||||||
|
"retained gameplay binding",
|
||||||
|
() => RetainedUiGameplayBinding.Create(
|
||||||
|
retainedUi.Host.Root,
|
||||||
|
(item, x, y) =>
|
||||||
|
selectionInteractions.PlaceDraggedItem(item, x, y),
|
||||||
|
d.HostQuiescence),
|
||||||
|
static value => value.Dispose());
|
||||||
|
retainedGameplayLease.Resource.Attach();
|
||||||
|
}
|
||||||
|
Fault(LivePresentationCompositionPoint.RetainedGameplayBound);
|
||||||
|
dispatcherLease.Resource.AlphaToCoverage =
|
||||||
|
d.Settings.ResolvedQuality.AlphaToCoverage;
|
||||||
|
|
||||||
|
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||||
|
PaperdollViewportRenderer>? paperdollLease = null;
|
||||||
|
PaperdollFramePresenter? paperdollPresenter = null;
|
||||||
|
if (interaction.RetainedUi?.Runtime.PaperdollViewportWidget is { } viewport
|
||||||
|
&& interaction.RetainedUi.Runtime.InventoryFrame is { } inventoryFrame)
|
||||||
|
{
|
||||||
|
paperdollLease = scope.Acquire(
|
||||||
|
"paperdoll viewport",
|
||||||
|
() => new PaperdollViewportRenderer(
|
||||||
|
d.Gl,
|
||||||
|
dispatcherLease.Resource,
|
||||||
|
foundation.SceneLighting,
|
||||||
|
foundation.TextureCache),
|
||||||
|
static value => value.Dispose());
|
||||||
|
IUiViewportRenderer? previousRenderer = viewport.Renderer;
|
||||||
|
viewport.Renderer = paperdollLease.Resource;
|
||||||
|
bindings.AdoptRelease(
|
||||||
|
"paperdoll viewport target",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(viewport.Renderer, paperdollLease.Resource))
|
||||||
|
viewport.Renderer = previousRenderer;
|
||||||
|
});
|
||||||
|
paperdollPresenter = new PaperdollFramePresenter(
|
||||||
|
paperdollLease.Resource,
|
||||||
|
new RetailPaperdollFrameView(
|
||||||
|
viewport,
|
||||||
|
new PaperdollInventoryVisibility(inventoryFrame)),
|
||||||
|
new RetailPaperdollDollFactory(
|
||||||
|
new LivePaperdollEntityLookup(liveEntities),
|
||||||
|
d.PlayerIdentity,
|
||||||
|
new RetailPaperdollPoseApplicator(
|
||||||
|
content.Dats,
|
||||||
|
content.AnimationLoader,
|
||||||
|
d.DatLock)));
|
||||||
|
}
|
||||||
|
Fault(LivePresentationCompositionPoint.PaperdollCreated);
|
||||||
|
|
||||||
|
var envCellFrustum = new WbFrustum();
|
||||||
|
var envCellLease = scope.Acquire(
|
||||||
|
"environment-cell renderer",
|
||||||
|
() => new EnvCellRenderer(
|
||||||
|
d.Gl,
|
||||||
|
foundation.MeshAdapter.MeshManager!,
|
||||||
|
envCellFrustum),
|
||||||
|
static value => value.Dispose());
|
||||||
|
envCellLease.Resource.Initialize(foundation.MeshShader);
|
||||||
|
Fault(LivePresentationCompositionPoint.EnvironmentCellsCreated);
|
||||||
|
|
||||||
|
var landblockRenderPublisher = new LandblockRenderPublisher(
|
||||||
|
(landblockId, meshData, origin) =>
|
||||||
|
foundation.Terrain.AddLandblockWithMesh(
|
||||||
|
landblockId,
|
||||||
|
meshData,
|
||||||
|
origin),
|
||||||
|
foundation.Terrain.RemoveLandblock,
|
||||||
|
d.CellVisibility,
|
||||||
|
worldState,
|
||||||
|
envCellLease.Resource.CommitLandblock,
|
||||||
|
build => EnvCellMeshPreparationScheduler.Schedule(
|
||||||
|
build,
|
||||||
|
foundation.MeshAdapter.MeshManager!),
|
||||||
|
envCellLease.Resource.RemoveLandblock);
|
||||||
|
var landblockPhysicsPublisher = new LandblockPhysicsPublisher(
|
||||||
|
d.PhysicsEngine,
|
||||||
|
world.TerrainBuild.HeightTable);
|
||||||
|
var landblockStaticPublisher =
|
||||||
|
new LandblockStaticPresentationPublisher(
|
||||||
|
content.LightingSink,
|
||||||
|
d.TranslucencyFades,
|
||||||
|
d.WorldGameState,
|
||||||
|
d.WorldEvents);
|
||||||
|
var landblockRetirementOwner =
|
||||||
|
new LandblockPresentationRetirementOwner(
|
||||||
|
landblockRenderPublisher,
|
||||||
|
landblockPhysicsPublisher,
|
||||||
|
landblockStaticPublisher,
|
||||||
|
content.LightingSink,
|
||||||
|
d.TranslucencyFades);
|
||||||
|
var landblockLoaded = new DeferredLiveEntityLandblockLoadedSink();
|
||||||
|
var landblockPipeline = new LandblockPresentationPipeline(
|
||||||
|
landblockRenderPublisher,
|
||||||
|
landblockPhysicsPublisher,
|
||||||
|
landblockStaticPublisher,
|
||||||
|
worldState,
|
||||||
|
landblockRetirementOwner,
|
||||||
|
landblockLoaded.OnLandblockLoaded,
|
||||||
|
landblockRenderPublisher.PrepareAfterRenderPins);
|
||||||
|
Fault(LivePresentationCompositionPoint.LandblockPipelineCreated);
|
||||||
|
|
||||||
|
var clipFrameLease = scope.Acquire(
|
||||||
|
"portal clip frame",
|
||||||
|
ClipFrame.NoClip,
|
||||||
|
static value => value.Dispose());
|
||||||
|
var portalDepthLease = scope.Acquire(
|
||||||
|
"portal depth mask",
|
||||||
|
() => new PortalDepthMaskRenderer(d.Gl),
|
||||||
|
static value => value.Dispose());
|
||||||
|
PortalTunnelPresentation portalTunnel;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
portalTunnel = d.PortalTunnelFallback.AcquirePrepared(
|
||||||
|
() => PortalTunnelPresentation.CreateRequired(
|
||||||
|
d.Gl,
|
||||||
|
content.Dats,
|
||||||
|
content.AnimationLoader,
|
||||||
|
d.HookRouter,
|
||||||
|
dispatcherLease.Resource,
|
||||||
|
foundation.SceneLighting,
|
||||||
|
foundation.MeshAdapter),
|
||||||
|
static tunnel => tunnel.PrepareResources());
|
||||||
|
}
|
||||||
|
catch (Exception acquisitionFailure)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
d.PortalTunnelFallback.ReleaseFallback();
|
||||||
|
}
|
||||||
|
catch (Exception cleanupFailure)
|
||||||
|
{
|
||||||
|
throw new AggregateException(
|
||||||
|
"Portal-tunnel construction and fallback rollback both failed.",
|
||||||
|
acquisitionFailure,
|
||||||
|
cleanupFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
var portalTunnelLease = scope.Own(
|
||||||
|
"portal tunnel fallback",
|
||||||
|
portalTunnel,
|
||||||
|
_ => d.PortalTunnelFallback.ReleaseFallback());
|
||||||
|
Fault(LivePresentationCompositionPoint.PortalResourcesCreated);
|
||||||
|
|
||||||
|
AcDream.App.Rendering.Shader skyShader =
|
||||||
|
d.RenderResourceLifetime.AcquireSkyShader(
|
||||||
|
() => new AcDream.App.Rendering.Shader(
|
||||||
|
d.Gl,
|
||||||
|
Path.Combine(foundation.ShadersDirectory, "sky.vert"),
|
||||||
|
Path.Combine(foundation.ShadersDirectory, "sky.frag")));
|
||||||
|
var skyShaderLease = scope.Own(
|
||||||
|
"sky shader lifetime",
|
||||||
|
skyShader,
|
||||||
|
_ => d.RenderResourceLifetime.ReleaseSkyShader());
|
||||||
|
var skyLease = scope.Acquire(
|
||||||
|
"sky renderer",
|
||||||
|
() => new SkyRenderer(
|
||||||
|
d.Gl,
|
||||||
|
content.Dats,
|
||||||
|
skyShader,
|
||||||
|
foundation.TextureCache,
|
||||||
|
foundation.Samplers),
|
||||||
|
static value => value.Dispose());
|
||||||
|
var particleLease = scope.Acquire(
|
||||||
|
"particle renderer",
|
||||||
|
() => new ParticleRenderer(
|
||||||
|
d.Gl,
|
||||||
|
foundation.ShadersDirectory,
|
||||||
|
content.ParticleSystem,
|
||||||
|
foundation.TextureCache,
|
||||||
|
content.Dats,
|
||||||
|
foundation.MeshAdapter,
|
||||||
|
d.RetailAlphaQueue),
|
||||||
|
static value => value.Dispose());
|
||||||
|
Fault(LivePresentationCompositionPoint.SkyAndParticlesCreated);
|
||||||
|
|
||||||
|
IRenderFrameResourceDiagnosticsSource? resourceDiagnostics =
|
||||||
|
d.Options.UiProbeDump
|
||||||
|
? new RuntimeRenderFrameResourceDiagnosticsSource(
|
||||||
|
content.ParticleSystem,
|
||||||
|
content.ParticleSink,
|
||||||
|
dispatcherLease.Resource,
|
||||||
|
envCellLease.Resource,
|
||||||
|
particleLease.Resource,
|
||||||
|
interaction.RetainedUi?.Host.TextRenderer,
|
||||||
|
portalDepthLease.Resource,
|
||||||
|
clipFrameLease.Resource,
|
||||||
|
foundation.Terrain,
|
||||||
|
foundation.SceneLighting,
|
||||||
|
foundation.MeshAdapter,
|
||||||
|
foundation.TextureCache)
|
||||||
|
: null;
|
||||||
|
var frameDiagnostics = new RenderFrameDiagnosticsController(
|
||||||
|
new RuntimeRenderFrameTitleFactsSource(
|
||||||
|
worldState,
|
||||||
|
d.AnimatedEntities,
|
||||||
|
d.WorldTime),
|
||||||
|
new SilkRenderFrameTitleSink(d.Window),
|
||||||
|
d.RenderDiagnosticLog,
|
||||||
|
d.Options.UiProbeDump,
|
||||||
|
resourceDiagnostics);
|
||||||
|
if (d.DevFrameDiagnostics is { } devFrameDiagnostics)
|
||||||
|
{
|
||||||
|
bindings.Adopt(
|
||||||
|
"developer frame diagnostics",
|
||||||
|
devFrameDiagnostics.BindOwned(frameDiagnostics));
|
||||||
|
}
|
||||||
|
bindings.Adopt(
|
||||||
|
"retained-UI frame diagnostics",
|
||||||
|
d.UiFrameDiagnostics.BindOwned(frameDiagnostics));
|
||||||
|
Fault(LivePresentationCompositionPoint.DiagnosticsBound);
|
||||||
|
|
||||||
|
var bindingsLease = scope.Own(
|
||||||
|
"live-presentation runtime bindings",
|
||||||
|
bindings,
|
||||||
|
static value => value.Dispose());
|
||||||
|
bindingsOwnedByScope = true;
|
||||||
|
var result = new LivePresentationResult(
|
||||||
|
componentLifecycle,
|
||||||
|
motionRuntime,
|
||||||
|
parentAcceptance,
|
||||||
|
entitySpawnAdapter,
|
||||||
|
entityScriptActivator,
|
||||||
|
staticAnimationScheduler,
|
||||||
|
worldState,
|
||||||
|
liveEntities,
|
||||||
|
projectileController,
|
||||||
|
projectionWithdrawal,
|
||||||
|
lightsLease.Resource,
|
||||||
|
animationScheduler,
|
||||||
|
animationPresenter,
|
||||||
|
equippedLease.Resource,
|
||||||
|
entityEffects,
|
||||||
|
presentationLease.Resource,
|
||||||
|
remoteTeleportLease.Resource,
|
||||||
|
dispatcherLease.Resource,
|
||||||
|
selectionScene,
|
||||||
|
selectionQuery,
|
||||||
|
selectionInteractions,
|
||||||
|
retainedGameplayLease?.Resource,
|
||||||
|
paperdollLease?.Resource,
|
||||||
|
paperdollPresenter,
|
||||||
|
envCellFrustum,
|
||||||
|
envCellLease.Resource,
|
||||||
|
landblockPipeline,
|
||||||
|
clipFrameLease.Resource,
|
||||||
|
portalDepthLease.Resource,
|
||||||
|
skyLease.Resource,
|
||||||
|
particleLease.Resource,
|
||||||
|
frameDiagnostics,
|
||||||
|
bindings,
|
||||||
|
landblockLoaded);
|
||||||
|
_publication.PublishLivePresentation(result);
|
||||||
|
|
||||||
|
liveRuntimeLease.Transfer();
|
||||||
|
lightsLease.Transfer();
|
||||||
|
equippedLease.Transfer();
|
||||||
|
presentationLease.Transfer();
|
||||||
|
remoteTeleportLease.Transfer();
|
||||||
|
dispatcherLease.Transfer();
|
||||||
|
retainedGameplayLease?.Transfer();
|
||||||
|
paperdollLease?.Transfer();
|
||||||
|
envCellLease.Transfer();
|
||||||
|
clipFrameLease.Transfer();
|
||||||
|
portalDepthLease.Transfer();
|
||||||
|
portalTunnelLease.Transfer();
|
||||||
|
skyLease.Transfer();
|
||||||
|
skyShaderLease.Transfer();
|
||||||
|
particleLease.Transfer();
|
||||||
|
bindingsLease.Transfer();
|
||||||
|
Fault(LivePresentationCompositionPoint.ResultPublished);
|
||||||
|
scope.Complete();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Fault(LivePresentationCompositionPoint point) =>
|
||||||
|
_faultInjection?.Invoke(point);
|
||||||
|
|
||||||
|
private sealed class NullAnimationLoader : IAnimationLoader
|
||||||
|
{
|
||||||
|
public static NullAnimationLoader Instance { get; } = new();
|
||||||
|
public Animation? LoadAnimation(uint id) => null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DeferredSelectionInteractionSource
|
||||||
|
{
|
||||||
|
public SelectionInteractionController? Current { get; private set; }
|
||||||
|
|
||||||
|
public void Bind(SelectionInteractionController value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
if (Current is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Live motion selection interactions are already bound.");
|
||||||
|
}
|
||||||
|
Current = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Composition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exact-owner leases installed by live-presentation composition. Successful
|
||||||
|
/// releases are removed immediately, so a failed cleanup retries only its
|
||||||
|
/// remaining suffix and never replays completed detach operations.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LivePresentationRuntimeBindings : 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 AdoptRelease(string name, Action release) =>
|
||||||
|
Adopt(name, new DelegateBinding(release));
|
||||||
|
|
||||||
|
public void BindProjectionVisibility(
|
||||||
|
LiveEntityRuntime source,
|
||||||
|
Action<LiveEntityRecord, bool> handler,
|
||||||
|
string name)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
ArgumentNullException.ThrowIfNull(handler);
|
||||||
|
source.ProjectionVisibilityChanged += handler;
|
||||||
|
Adopt(name, new DelegateBinding(
|
||||||
|
() => source.ProjectionVisibilityChanged -= handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BindProjectionPoseReady(
|
||||||
|
EquippedChildRenderController source,
|
||||||
|
Action<uint> handler)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
ArgumentNullException.ThrowIfNull(handler);
|
||||||
|
source.ProjectionPoseReady += handler;
|
||||||
|
Adopt("equipped-child projection pose", new DelegateBinding(
|
||||||
|
() => source.ProjectionPoseReady -= handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
$"Live-presentation binding '{name}' did not detach.",
|
||||||
|
failure));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures is not null)
|
||||||
|
{
|
||||||
|
throw new AggregateException(
|
||||||
|
"Live-presentation binding cleanup remains incomplete.",
|
||||||
|
failures);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DelegateBinding : IDisposable
|
||||||
|
{
|
||||||
|
private Action? _release;
|
||||||
|
|
||||||
|
public DelegateBinding(Action release) =>
|
||||||
|
_release = release ?? throw new ArgumentNullException(nameof(release));
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Action? release = _release;
|
||||||
|
if (release is null)
|
||||||
|
return;
|
||||||
|
release();
|
||||||
|
_release = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,6 +39,18 @@ internal sealed class DeferredLiveEntityMotionRuntimeBindings
|
||||||
_target = target;
|
_target = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(ILiveEntityMotionRuntimeBindings target)
|
||||||
|
{
|
||||||
|
Bind(target);
|
||||||
|
return new Binding(this, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unbind(ILiveEntityMotionRuntimeBindings expected)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_target, expected))
|
||||||
|
_target = null;
|
||||||
|
}
|
||||||
|
|
||||||
private ILiveEntityMotionRuntimeBindings Target =>
|
private ILiveEntityMotionRuntimeBindings Target =>
|
||||||
_target ?? throw new InvalidOperationException(
|
_target ?? throw new InvalidOperationException(
|
||||||
"Live entity motion runtime bindings were used before binding.");
|
"Live entity motion runtime bindings were used before binding.");
|
||||||
|
|
@ -61,4 +73,21 @@ internal sealed class DeferredLiveEntityMotionRuntimeBindings
|
||||||
|
|
||||||
public IPhysicsObjHost? ResolvePhysicsHost(uint serverGuid) =>
|
public IPhysicsObjHost? ResolvePhysicsHost(uint serverGuid) =>
|
||||||
Target.ResolvePhysicsHost(serverGuid);
|
Target.ResolvePhysicsHost(serverGuid);
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private DeferredLiveEntityMotionRuntimeBindings? _owner;
|
||||||
|
private readonly ILiveEntityMotionRuntimeBindings _expected;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
DeferredLiveEntityMotionRuntimeBindings owner,
|
||||||
|
ILiveEntityMotionRuntimeBindings expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,12 @@ internal sealed class DeferredCanonicalWorldEntityCountSource
|
||||||
_target = target;
|
_target = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(GpuWorldState target)
|
||||||
|
{
|
||||||
|
Bind(target);
|
||||||
|
return new Binding(this, target);
|
||||||
|
}
|
||||||
|
|
||||||
public void Unbind(GpuWorldState target)
|
public void Unbind(GpuWorldState target)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(target);
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
|
@ -115,6 +121,23 @@ internal sealed class DeferredCanonicalWorldEntityCountSource
|
||||||
_deactivated = true;
|
_deactivated = true;
|
||||||
_target = null;
|
_target = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private DeferredCanonicalWorldEntityCountSource? _owner;
|
||||||
|
private readonly GpuWorldState _target;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
DeferredCanonicalWorldEntityCountSource owner,
|
||||||
|
GpuWorldState target)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface IDevToolsPlayerModeCommands
|
internal interface IDevToolsPlayerModeCommands
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ public sealed class GameWindow :
|
||||||
IGameWindowContentEffectsAudioPublication,
|
IGameWindowContentEffectsAudioPublication,
|
||||||
IGameWindowSettingsDevToolsPublication,
|
IGameWindowSettingsDevToolsPublication,
|
||||||
IGameWindowWorldRenderPublication,
|
IGameWindowWorldRenderPublication,
|
||||||
IGameWindowInteractionRetainedUiPublication
|
IGameWindowInteractionRetainedUiPublication,
|
||||||
|
IGameWindowLivePresentationPublication
|
||||||
{
|
{
|
||||||
private static double ClientTimerNow() =>
|
private static double ClientTimerNow() =>
|
||||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||||
|
|
@ -97,6 +98,7 @@ public sealed class GameWindow :
|
||||||
_debugVmRenderFacts = new();
|
_debugVmRenderFacts = new();
|
||||||
private AcDream.App.Rendering.RenderFrameDiagnosticsController?
|
private AcDream.App.Rendering.RenderFrameDiagnosticsController?
|
||||||
_renderFrameDiagnostics;
|
_renderFrameDiagnostics;
|
||||||
|
private LivePresentationRuntimeBindings? _livePresentationBindings;
|
||||||
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
|
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
|
||||||
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
|
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
|
||||||
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
||||||
|
|
@ -924,6 +926,76 @@ public sealed class GameWindow :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IGameWindowLivePresentationPublication.PublishLivePresentation(
|
||||||
|
LivePresentationResult result)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(result);
|
||||||
|
if (_liveEntities is not null
|
||||||
|
|| _wbEntitySpawnAdapter is not null
|
||||||
|
|| _entityScriptActivator is not null
|
||||||
|
|| _staticAnimationScheduler is not null
|
||||||
|
|| _wbDrawDispatcher is not null
|
||||||
|
|| _retailSelectionScene is not null
|
||||||
|
|| _worldSelectionQuery is not null
|
||||||
|
|| _selectionInteractions is not null
|
||||||
|
|| _retainedUiGameplayBinding is not null
|
||||||
|
|| _equippedChildRenderer is not null
|
||||||
|
|| _entityEffects is not null
|
||||||
|
|| _liveEntityPresentation is not null
|
||||||
|
|| _remoteTeleportController is not null
|
||||||
|
|| _projectileController is not null
|
||||||
|
|| _liveEntityProjectionWithdrawal is not null
|
||||||
|
|| _liveEntityLights is not null
|
||||||
|
|| _liveAnimationScheduler is not null
|
||||||
|
|| _animationPresenter is not null
|
||||||
|
|| _paperdollViewportRenderer is not null
|
||||||
|
|| _paperdollFramePresenter is not null
|
||||||
|
|| _envCellRenderer is not null
|
||||||
|
|| _envCellFrustum is not null
|
||||||
|
|| _landblockPresentationPipeline is not null
|
||||||
|
|| _portalDepthMask is not null
|
||||||
|
|| _clipFrame is not null
|
||||||
|
|| _skyRenderer is not null
|
||||||
|
|| _particleRenderer is not null
|
||||||
|
|| _renderFrameDiagnostics is not null
|
||||||
|
|| _livePresentationBindings is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The GameWindow composition shell already owns live presentation state.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_wbEntitySpawnAdapter = result.EntitySpawnAdapter;
|
||||||
|
_entityScriptActivator = result.EntityScriptActivator;
|
||||||
|
_staticAnimationScheduler = result.StaticAnimationScheduler;
|
||||||
|
_worldState = result.WorldState;
|
||||||
|
_liveEntities = result.LiveEntities;
|
||||||
|
_projectileController = result.ProjectileController;
|
||||||
|
_liveEntityProjectionWithdrawal = result.ProjectionWithdrawal;
|
||||||
|
_liveEntityLights = result.Lights;
|
||||||
|
_liveAnimationScheduler = result.AnimationScheduler;
|
||||||
|
_animationPresenter = result.AnimationPresenter;
|
||||||
|
_equippedChildRenderer = result.EquippedChildren;
|
||||||
|
_entityEffects = result.EntityEffects;
|
||||||
|
_liveEntityPresentation = result.Presentation;
|
||||||
|
_remoteTeleportController = result.RemoteTeleport;
|
||||||
|
_wbDrawDispatcher = result.DrawDispatcher;
|
||||||
|
_retailSelectionScene = result.SelectionScene;
|
||||||
|
_worldSelectionQuery = result.SelectionQuery;
|
||||||
|
_selectionInteractions = result.SelectionInteractions;
|
||||||
|
_retainedUiGameplayBinding = result.RetainedGameplay;
|
||||||
|
_paperdollViewportRenderer = result.PaperdollRenderer;
|
||||||
|
_paperdollFramePresenter = result.PaperdollPresenter;
|
||||||
|
_envCellFrustum = result.EnvCellFrustum;
|
||||||
|
_envCellRenderer = result.EnvCellRenderer;
|
||||||
|
_landblockPresentationPipeline = result.LandblockPipeline;
|
||||||
|
_clipFrame = result.ClipFrame;
|
||||||
|
_portalDepthMask = result.PortalDepthMask;
|
||||||
|
_skyRenderer = result.SkyRenderer;
|
||||||
|
_particleRenderer = result.ParticleRenderer;
|
||||||
|
_renderFrameDiagnostics = result.FrameDiagnostics;
|
||||||
|
_livePresentationBindings = result.RuntimeBindings;
|
||||||
|
}
|
||||||
|
|
||||||
private static void PublishCompositionOwner<T>(
|
private static void PublishCompositionOwner<T>(
|
||||||
ref T? destination,
|
ref T? destination,
|
||||||
T value,
|
T value,
|
||||||
|
|
@ -1124,532 +1196,52 @@ public sealed class GameWindow :
|
||||||
settingsDevTools,
|
settingsDevTools,
|
||||||
worldRender);
|
worldRender);
|
||||||
|
|
||||||
AcDream.App.World.DeferredLiveEntityParentAcceptance? parentAcceptance = null;
|
LivePresentationResult livePresentation =
|
||||||
// Phase N.4 Task 12: construct LandblockSpawnAdapter under the feature flag
|
new LivePresentationCompositionPhase(
|
||||||
// and rebuild _worldState so it threads the adapter in. _worldState starts
|
new LivePresentationDependencies(
|
||||||
// as an unadorned GpuWorldState (field initializer); here we replace it with
|
_options,
|
||||||
// one that carries the adapter so AddLandblock/RemoveLandblock notify WB.
|
platform.Graphics,
|
||||||
// Phase N.4 Task 17: also construct EntitySpawnAdapter for server-spawned
|
_window!,
|
||||||
// per-instance content under the same flag.
|
_datLock,
|
||||||
// N.5 mandatory path: spawn adapters + dispatcher always construct.
|
_runtimeSettings,
|
||||||
// _wbMeshAdapter, _meshShader, _textureCache, and _bindlessSupport are
|
_hostQuiescence,
|
||||||
// all guaranteed non-null here (startup throws above if any are missing).
|
|
||||||
var liveEntityComponentLifecycle =
|
|
||||||
new AcDream.App.World.DeferredLiveEntityRuntimeComponentLifecycle();
|
|
||||||
AcDream.App.Physics.LiveEntityMotionRuntimeController?
|
|
||||||
liveEntityMotionRuntime = null;
|
|
||||||
{
|
|
||||||
var wbSpawnAdapter = new AcDream.App.Rendering.Wb.LandblockSpawnAdapter(_wbMeshAdapter!);
|
|
||||||
// Sequencer factory: look up Setup + MotionTable from dats and build
|
|
||||||
// an AnimationSequencer. Falls back to a no-op sequencer when the
|
|
||||||
// entity has no motion table (static props, etc.). Uses _animLoader
|
|
||||||
// which is initialised earlier in OnLoad; it is non-null here.
|
|
||||||
var capturedDats = _dats;
|
|
||||||
var capturedAnimLoader = _animLoader;
|
|
||||||
AcDream.Core.Physics.AnimationSequencer SequencerFactory(AcDream.Core.World.WorldEntity e)
|
|
||||||
{
|
|
||||||
if (capturedDats is not null && capturedAnimLoader is not null)
|
|
||||||
{
|
|
||||||
var setup = capturedDats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
|
|
||||||
if (setup is not null)
|
|
||||||
{
|
|
||||||
uint mtableId = (uint)setup.DefaultMotionTable;
|
|
||||||
if (mtableId != 0)
|
|
||||||
{
|
|
||||||
var mtable = capturedDats.Get<DatReaderWriter.DBObjs.MotionTable>(mtableId);
|
|
||||||
if (mtable is not null)
|
|
||||||
return new AcDream.Core.Physics.AnimationSequencer(setup, mtable, capturedAnimLoader);
|
|
||||||
}
|
|
||||||
// Setup exists but no motion table — no-op sequencer.
|
|
||||||
return new AcDream.Core.Physics.AnimationSequencer(
|
|
||||||
setup,
|
|
||||||
new DatReaderWriter.DBObjs.MotionTable(),
|
|
||||||
capturedAnimLoader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Complete fallback: empty setup + empty motion table + null loader.
|
|
||||||
return new AcDream.Core.Physics.AnimationSequencer(
|
|
||||||
new DatReaderWriter.DBObjs.Setup(),
|
|
||||||
new DatReaderWriter.DBObjs.MotionTable(),
|
|
||||||
new NullAnimLoader());
|
|
||||||
}
|
|
||||||
var wbEntitySpawnAdapter = new AcDream.App.Rendering.Wb.EntitySpawnAdapter(
|
|
||||||
_textureCache!, SequencerFactory, _wbMeshAdapter!);
|
|
||||||
_wbEntitySpawnAdapter = wbEntitySpawnAdapter;
|
|
||||||
|
|
||||||
// Phase C.1.5a/b: construct EntityScriptActivator so static entities
|
|
||||||
// (server-spawned AND dat-hydrated) fire Setup.DefaultScript through
|
|
||||||
// the PhysicsScriptRunner on enter-world. C.1.5b adds per-part
|
|
||||||
// transforms from the entity's exact current MeshRefs so
|
|
||||||
// multi-emitter scripts distribute across mesh parts (closes #56).
|
|
||||||
// Animated frames replace this snapshot through EntityEffectPoseRegistry.
|
|
||||||
// _scriptRunner and
|
|
||||||
// _particleSink are initialised earlier in OnLoad (line ~1083); both
|
|
||||||
// are non-null here. The resolver lambda captures _dats and swallows
|
|
||||||
// dat-lookup throws — see C.1.5a spec §6 (error handling) for rationale.
|
|
||||||
AcDream.App.Rendering.Vfx.EntityEffectController? entityEffects = null;
|
|
||||||
var staticLiveRootCommitter = new StaticLiveRootCommitter(
|
|
||||||
_liveEntityRuntimeSlot,
|
|
||||||
_physicsEngine.ShadowObjects,
|
|
||||||
_liveWorldOrigin,
|
|
||||||
_effectPoses);
|
|
||||||
var staticAnimationResidency = new LiveStaticAnimationResidency(
|
|
||||||
_liveEntityRuntimeSlot);
|
|
||||||
_staticAnimationScheduler = new RetailStaticAnimatingObjectScheduler(
|
|
||||||
capturedAnimLoader!,
|
|
||||||
_animationHookFrames!.Capture,
|
|
||||||
_effectPoses.Publish,
|
|
||||||
staticAnimationResidency.IsResident,
|
|
||||||
(entity, body) =>
|
|
||||||
_ = staticLiveRootCommitter.Commit(entity, body),
|
|
||||||
staticAnimationResidency.ProjectionVersion);
|
|
||||||
AcDream.App.Rendering.Vfx.ScriptActivationInfo? ResolveActivation(AcDream.Core.World.WorldEntity e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
DatReaderWriter.DBObjs.Setup? setup =
|
|
||||||
capturedDats?.Get<DatReaderWriter.DBObjs.Setup>(
|
|
||||||
e.SourceGfxObjOrSetupId);
|
|
||||||
if (setup is null) return null;
|
|
||||||
uint scriptId = setup.DefaultScript.DataId;
|
|
||||||
if (e.IndexedPartTransforms.Count == 0)
|
|
||||||
{
|
|
||||||
var indexed = AcDream.App.Rendering.Vfx.IndexedSetupPartPoseBuilder.Build(
|
|
||||||
setup,
|
|
||||||
e);
|
|
||||||
e.SetIndexedPartPoses(indexed.Poses, indexed.Available);
|
|
||||||
}
|
|
||||||
IReadOnlyList<System.Numerics.Matrix4x4> parts = e.IndexedPartTransforms;
|
|
||||||
IReadOnlyList<bool> availability = e.IndexedPartAvailable;
|
|
||||||
var profile = AcDream.App.Rendering.Vfx.EntityEffectProfile.CreateDatStatic(setup);
|
|
||||||
bool usesStaticAnimationWorkset = e.ServerGuid == 0
|
|
||||||
|| (_liveEntities?.TryGetRecord(
|
|
||||||
e.ServerGuid,
|
|
||||||
out LiveEntityRecord liveRecord) == true
|
|
||||||
&& (liveRecord.FinalPhysicsState
|
|
||||||
& AcDream.Core.Physics.PhysicsStateFlags.Static) != 0);
|
|
||||||
return new AcDream.App.Rendering.Vfx.ScriptActivationInfo(
|
|
||||||
scriptId,
|
|
||||||
parts,
|
|
||||||
profile,
|
|
||||||
availability,
|
|
||||||
setup,
|
|
||||||
(uint)setup.DefaultAnimation,
|
|
||||||
usesStaticAnimationWorkset);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var entityScriptActivator = new AcDream.App.Rendering.Vfx.EntityScriptActivator(
|
|
||||||
_scriptRunner!,
|
|
||||||
_particleSink!,
|
|
||||||
_effectPoses,
|
|
||||||
ResolveActivation,
|
|
||||||
(ownerId, entity, profile) =>
|
|
||||||
entityEffects?.OnDatStaticEntityReady(ownerId, entity, profile),
|
|
||||||
ownerId =>
|
|
||||||
{
|
|
||||||
entityEffects?.OnDatStaticEntityRemoved(ownerId);
|
|
||||||
_lightingSink?.UnregisterOwner(ownerId);
|
|
||||||
_translucencyFades.ClearEntity(ownerId);
|
|
||||||
},
|
|
||||||
(entity, info) => _staticAnimationScheduler.Register(entity, info),
|
|
||||||
ownerId => _staticAnimationScheduler.Unregister(ownerId),
|
|
||||||
(entity, info) => _staticAnimationScheduler.Rebind(entity, info));
|
|
||||||
_entityScriptActivator = entityScriptActivator;
|
|
||||||
|
|
||||||
// Phase Post-A.5 #53 (Task 12): wire EntityClassificationCache.InvalidateLandblock
|
|
||||||
// so Tier 1 cache entries get swept on LB demote (Near to Far) and unload.
|
|
||||||
// Per spec §5.3 W3b. The callback receives the canonical landblock id
|
|
||||||
// matching the LandblockHint stored at Populate time.
|
|
||||||
_worldState = new AcDream.App.Streaming.GpuWorldState(
|
|
||||||
wbSpawnAdapter,
|
|
||||||
onLandblockUnloaded: _classificationCache.InvalidateLandblock,
|
|
||||||
entityScriptActivator: entityScriptActivator);
|
|
||||||
_devToolsComposition?.LateBindings.WorldEntities.Bind(_worldState);
|
|
||||||
_liveEntities = new AcDream.App.World.LiveEntityRuntime(
|
|
||||||
_worldState,
|
|
||||||
new AcDream.App.World.CompositeLiveEntityResourceLifecycle(
|
|
||||||
new(
|
|
||||||
entity => wbEntitySpawnAdapter.OnCreate(entity),
|
|
||||||
entity => wbEntitySpawnAdapter.OnRemove(entity)),
|
|
||||||
new(
|
|
||||||
entityScriptActivator.OnCreate,
|
|
||||||
entityScriptActivator.OnRemove)),
|
|
||||||
liveEntityComponentLifecycle);
|
|
||||||
_liveEntityRuntimeSlot.Bind(_liveEntities);
|
|
||||||
liveEntityMotionRuntime =
|
|
||||||
new AcDream.App.Physics.LiveEntityMotionRuntimeController(
|
|
||||||
_liveEntities,
|
|
||||||
_physicsDataCache,
|
|
||||||
() => _selectionInteractions,
|
|
||||||
_selection,
|
|
||||||
_liveWorldOrigin);
|
|
||||||
_liveEntityMotionBindings.Bind(liveEntityMotionRuntime);
|
|
||||||
_liveEntities.ProjectionVisibilityChanged += (record, visible) =>
|
|
||||||
{
|
|
||||||
if (record.WorldEntity is { } entity)
|
|
||||||
wbEntitySpawnAdapter.SetPresentationResident(entity, visible);
|
|
||||||
};
|
|
||||||
_liveEntities.ProjectionVisibilityChanged += (record, visible) =>
|
|
||||||
{
|
|
||||||
if (record.WorldEntity is { } entity)
|
|
||||||
_particleSink!.SetEntityPresentationVisible(entity.Id, visible);
|
|
||||||
};
|
|
||||||
_projectileController = new AcDream.App.Physics.ProjectileController(
|
|
||||||
_liveEntities,
|
|
||||||
_physicsEngine,
|
_physicsEngine,
|
||||||
new AcDream.App.Physics.DatProjectileSetupResolver(_dats!, _datLock),
|
_physicsDataCache,
|
||||||
new EntityRootPosePublisher(_effectPoses),
|
|
||||||
_liveWorldOrigin)
|
|
||||||
{
|
|
||||||
DiagnosticSink = message => Console.Error.WriteLine($"projectile: {message}"),
|
|
||||||
};
|
|
||||||
_liveEntityProjectionWithdrawal =
|
|
||||||
new AcDream.App.World.LiveEntityProjectionWithdrawalController(
|
|
||||||
_liveEntities,
|
|
||||||
_projectileController,
|
|
||||||
_worldGameState,
|
_worldGameState,
|
||||||
_worldEvents,
|
_worldEvents,
|
||||||
_physicsEngine.ShadowObjects,
|
|
||||||
_effectPoses,
|
|
||||||
_localPlayerShadow);
|
|
||||||
_liveEntityLights = new AcDream.App.Rendering.Vfx.LiveEntityLightController(
|
|
||||||
_liveEntities,
|
|
||||||
_effectPoses,
|
|
||||||
_lightingSink!,
|
|
||||||
setupId => _dats!.Get<DatReaderWriter.DBObjs.Setup>(setupId));
|
|
||||||
|
|
||||||
var ordinaryPhysicsUpdater =
|
|
||||||
new AcDream.App.Physics.LiveEntityOrdinaryPhysicsUpdater(
|
|
||||||
_physicsEngine,
|
|
||||||
_liveEntityMotionBindings.GetSetupCylinder);
|
|
||||||
_liveAnimationScheduler = new LiveEntityAnimationScheduler(
|
|
||||||
_liveEntities,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_remotePhysicsUpdater,
|
|
||||||
ordinaryPhysicsUpdater,
|
|
||||||
_projectileController,
|
|
||||||
new EntityRootPosePublisher(_effectPoses),
|
|
||||||
new AnimationHookCaptureSink(_animationHookFrames!));
|
|
||||||
_animationPresenter = new LiveEntityAnimationPresenter(
|
|
||||||
_liveEntities,
|
|
||||||
_staticAnimationScheduler,
|
|
||||||
_effectPoses,
|
|
||||||
new LiveAnimationPresentationContext(
|
|
||||||
_liveEntities,
|
|
||||||
_localPlayerIdentity,
|
|
||||||
_playerControllerSlot),
|
|
||||||
_animationDiagnostics,
|
|
||||||
_options.HidePartIndex);
|
|
||||||
|
|
||||||
parentAcceptance = new AcDream.App.World.DeferredLiveEntityParentAcceptance();
|
|
||||||
_equippedChildRenderer = new AcDream.App.Rendering.EquippedChildRenderController(
|
|
||||||
_dats!,
|
|
||||||
_datLock,
|
|
||||||
Objects,
|
|
||||||
_liveEntities,
|
|
||||||
_effectPoses,
|
|
||||||
parentAcceptance.TryAccept,
|
|
||||||
(childRecord, positionVersion, projectionVersion) =>
|
|
||||||
_liveEntityProjectionWithdrawal!.WithdrawExact(
|
|
||||||
childRecord,
|
|
||||||
positionVersion,
|
|
||||||
projectionVersion,
|
|
||||||
_playerServerGuid));
|
|
||||||
|
|
||||||
var tableResolver = new AcDream.Core.Vfx.PhysicsScriptTableResolver(
|
|
||||||
id => _dats!.Get<DatReaderWriter.DBObjs.PhysicsScriptTable>(id));
|
|
||||||
entityEffects = new AcDream.App.Rendering.Vfx.EntityEffectController(
|
|
||||||
_liveEntities,
|
|
||||||
_scriptRunner!,
|
|
||||||
tableResolver,
|
|
||||||
_effectPoses,
|
|
||||||
(parentLocalId, partIndex) =>
|
|
||||||
_equippedChildRenderer.FindChildLocalIdAtPart(parentLocalId, partIndex),
|
|
||||||
childLocalId => _equippedChildRenderer.FindParentLocalId(childLocalId),
|
|
||||||
ownerId => _entitySoundTables?.Remove(ownerId),
|
|
||||||
(ownerId, soundTableDid) =>
|
|
||||||
{
|
|
||||||
_entitySoundTables?.Remove(ownerId);
|
|
||||||
if (soundTableDid is { } did)
|
|
||||||
_entitySoundTables?.Set(ownerId, did);
|
|
||||||
});
|
|
||||||
_entityEffects = entityEffects;
|
|
||||||
_entityEffectAdvance.Bind(entityEffects);
|
|
||||||
entityEffects.DiagnosticSink = message =>
|
|
||||||
Console.Error.WriteLine($"vfx: {message}");
|
|
||||||
var partArrayLifecycle =
|
|
||||||
new AcDream.App.Rendering.LiveEntityPartArrayLifecycle(
|
|
||||||
_animatedEntities);
|
|
||||||
_liveEntityPresentation = new AcDream.App.World.LiveEntityPresentationController(
|
|
||||||
_liveEntities,
|
|
||||||
_physicsEngine.ShadowObjects,
|
|
||||||
entityEffects.PlayTypedFromHiddenTransition,
|
|
||||||
_equippedChildRenderer.SetDirectChildrenNoDraw,
|
|
||||||
_liveEntityMotionBindings.ClearTargetForHiddenEntity,
|
|
||||||
_liveWorldOrigin.GetCenter,
|
|
||||||
handlePartArrayEnterWorld: partArrayLifecycle.HandleEnterWorld);
|
|
||||||
var remoteShadowPlacement =
|
|
||||||
new AcDream.App.Physics.RemoteShadowPlacementSynchronizer(
|
|
||||||
_remotePhysicsUpdater,
|
|
||||||
_liveWorldOrigin);
|
|
||||||
var remoteTeleportPresentation =
|
|
||||||
new AcDream.App.Physics.RemoteTeleportPlacementPresentation(
|
|
||||||
_liveEntityPresentation);
|
|
||||||
_remoteTeleportController = new AcDream.App.Physics.RemoteTeleportController(
|
|
||||||
_physicsEngine,
|
|
||||||
_liveEntities,
|
|
||||||
_liveEntityMotionBindings.GetSetupCylinder,
|
|
||||||
_liveWorldOrigin.CellLocalForSeed,
|
|
||||||
remoteShadowPlacement.Sync,
|
|
||||||
remoteTeleportPresentation.Complete,
|
|
||||||
remoteTeleportPresentation.Begin);
|
|
||||||
_equippedChildRenderer.ProjectionPoseReady += guid =>
|
|
||||||
_liveEntityLights.OnAttachedPoseReady(guid);
|
|
||||||
_hookRegistrations!.Register(entityEffects);
|
|
||||||
|
|
||||||
_wbDrawDispatcher = new AcDream.App.Rendering.Wb.WbDrawDispatcher(
|
|
||||||
_gl, _meshShader!, _textureCache!, _wbMeshAdapter!, _wbEntitySpawnAdapter, _bindlessSupport!,
|
|
||||||
_classificationCache, _translucencyFades,
|
|
||||||
_retailSelectionScene ??= new AcDream.App.Rendering.Selection.RetailSelectionScene(
|
|
||||||
new AcDream.App.Rendering.Selection.RetailSelectionGeometryCache(
|
|
||||||
_dats!, _datLock)),
|
|
||||||
_retailAlphaQueue);
|
|
||||||
_worldSelectionQuery ??= new AcDream.App.Interaction.WorldSelectionQuery(
|
|
||||||
_liveEntities,
|
|
||||||
Objects,
|
|
||||||
_retailSelectionScene,
|
|
||||||
() => _localPlayerIdentity.ServerGuid,
|
|
||||||
interactionUi.LateBindings.SelectionCamera.Snapshot,
|
|
||||||
() => new System.Numerics.Vector2(
|
|
||||||
_pointerPosition.X,
|
|
||||||
_pointerPosition.Y),
|
|
||||||
() => _playerControllerSlot.Controller is { } player
|
|
||||||
? new AcDream.App.Interaction.PlayerInteractionPose(
|
|
||||||
player.CellId,
|
|
||||||
player.Position)
|
|
||||||
: null,
|
|
||||||
_liveEntityMotionBindings.GetSetupCylinder,
|
|
||||||
setupId =>
|
|
||||||
{
|
|
||||||
lock (_datLock)
|
|
||||||
{
|
|
||||||
if (!contentEffectsAudio.Dats.TryGet<DatReaderWriter.DBObjs.Setup>(
|
|
||||||
setupId,
|
|
||||||
out var setup)
|
|
||||||
|| setup.SelectionSphere is not { } sphere)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (sphere.Origin, sphere.Radius);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var radarSnapshotProvider = new AcDream.App.UI.Layout.RadarSnapshotProvider(
|
|
||||||
Objects,
|
|
||||||
() => _liveEntities.WorldEntities,
|
|
||||||
() => _liveEntities.Snapshots,
|
|
||||||
playerGuid: () => _localPlayerIdentity.ServerGuid,
|
|
||||||
playerYawRadians: () => _playerControllerSlot.Controller?.Yaw ?? 0f,
|
|
||||||
playerCellId: () => _playerControllerSlot.Controller?.CellId ?? 0u,
|
|
||||||
selectedGuid: () => _selection.SelectedObjectId,
|
|
||||||
coordinatesOnRadar: () =>
|
|
||||||
_runtimeSettings.Gameplay.CoordinatesOnRadar,
|
|
||||||
uiLocked: () => _runtimeSettings.Gameplay.LockUI,
|
|
||||||
playerEntities: () => _liveEntities.MaterializedWorldEntities,
|
|
||||||
spatialQuery: () => _worldState);
|
|
||||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
|
||||||
"radar snapshot",
|
|
||||||
interactionUi.LateBindings.Radar.Bind(radarSnapshotProvider));
|
|
||||||
if (_itemInteractionController is { } itemInteractions)
|
|
||||||
{
|
|
||||||
_selectionInteractions ??= new AcDream.App.Interaction.SelectionInteractionController(
|
|
||||||
_selection,
|
_selection,
|
||||||
_worldSelectionQuery,
|
Objects,
|
||||||
itemInteractions,
|
_liveEntityRuntimeSlot,
|
||||||
new AcDream.App.Interaction.WorldSessionSelectionInteractionTransport(
|
_liveEntityMotionBindings,
|
||||||
() => interactionUi.LateBindings.Session.CurrentSession),
|
_entityEffectAdvance,
|
||||||
new AcDream.App.Interaction.PlayerInteractionMovementSink(
|
_effectPoses,
|
||||||
() => _playerControllerSlot.Controller,
|
_remotePhysicsUpdater,
|
||||||
_playerApproachCompletions),
|
_localPlayerShadow,
|
||||||
compositionToast,
|
|
||||||
_playerApproachCompletions);
|
|
||||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
|
||||||
"world selection",
|
|
||||||
interactionUi.LateBindings.Selection.Bind(
|
|
||||||
_worldSelectionQuery,
|
|
||||||
_selectionInteractions));
|
|
||||||
}
|
|
||||||
if (_uiHost is { } retainedHost
|
|
||||||
&& _selectionInteractions is { } selectionInteractions)
|
|
||||||
{
|
|
||||||
_retainedUiGameplayBinding =
|
|
||||||
AcDream.App.Input.RetainedUiGameplayBinding.Create(
|
|
||||||
retainedHost.Root,
|
|
||||||
(item, x, y) =>
|
|
||||||
selectionInteractions.PlaceDraggedItem(item, x, y),
|
|
||||||
_hostQuiescence);
|
|
||||||
_retainedUiGameplayBinding.Attach();
|
|
||||||
}
|
|
||||||
// A.5 T22.5: apply A2C gate from quality preset.
|
|
||||||
_wbDrawDispatcher.AlphaToCoverage =
|
|
||||||
_runtimeSettings.ResolvedQuality.AlphaToCoverage;
|
|
||||||
|
|
||||||
// Slice 2: now that the dispatcher exists, build the paperdoll doll RTT renderer and hand it to
|
|
||||||
// the viewport widget. Reuses the dispatcher + the scene-lighting UBO + _gl. The widget only
|
|
||||||
// blits the texture; the pre-UI hook drives the 3-D pass (see OnRender).
|
|
||||||
if (_retailUiRuntime?.PaperdollViewportWidget is { } paperdollViewport
|
|
||||||
&& _retailUiRuntime.InventoryFrame is { } paperdollInventoryFrame
|
|
||||||
&& _sceneLightingUbo is not null)
|
|
||||||
{
|
|
||||||
_paperdollViewportRenderer = new AcDream.App.Rendering.PaperdollViewportRenderer(
|
|
||||||
_gl, _wbDrawDispatcher, _sceneLightingUbo, _textureCache!);
|
|
||||||
paperdollViewport.Renderer = _paperdollViewportRenderer;
|
|
||||||
_paperdollFramePresenter =
|
|
||||||
new AcDream.App.Rendering.PaperdollFramePresenter(
|
|
||||||
_paperdollViewportRenderer,
|
|
||||||
new AcDream.App.Rendering.RetailPaperdollFrameView(
|
|
||||||
paperdollViewport,
|
|
||||||
new AcDream.App.Rendering.PaperdollInventoryVisibility(
|
|
||||||
paperdollInventoryFrame)),
|
|
||||||
new AcDream.App.Rendering.RetailPaperdollDollFactory(
|
|
||||||
new AcDream.App.Rendering.LivePaperdollEntityLookup(
|
|
||||||
_liveEntities),
|
|
||||||
_localPlayerIdentity,
|
|
||||||
new AcDream.App.Rendering.RetailPaperdollPoseApplicator(
|
|
||||||
_dats!,
|
|
||||||
_animLoader!,
|
|
||||||
_datLock)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase A8: EnvCellRenderer init. Shares _meshShader (mesh_modern.{vert,frag})
|
|
||||||
// with the dispatcher — both consume the same global mesh buffer (VAO/IBO)
|
|
||||||
// from ObjectMeshManager.GlobalBuffer.
|
|
||||||
_envCellFrustum = new AcDream.App.Rendering.Wb.WbFrustum();
|
|
||||||
_envCellRenderer = new AcDream.App.Rendering.Wb.EnvCellRenderer(
|
|
||||||
_gl, _wbMeshAdapter!.MeshManager!, _envCellFrustum);
|
|
||||||
_envCellRenderer.Initialize(_meshShader!);
|
|
||||||
var landblockRenderPublisher = new AcDream.App.Streaming.LandblockRenderPublisher(
|
|
||||||
publishTerrain: (landblockId, meshData, origin) =>
|
|
||||||
_terrain!.AddLandblockWithMesh(landblockId, meshData, origin),
|
|
||||||
removeTerrain: landblockId => _terrain!.RemoveLandblock(landblockId),
|
|
||||||
cellVisibility: _cellVisibility,
|
|
||||||
worldState: _worldState,
|
|
||||||
commitEnvCells: _envCellRenderer.CommitLandblock,
|
|
||||||
prepareEnvCells: build =>
|
|
||||||
AcDream.App.Rendering.Wb.EnvCellMeshPreparationScheduler.Schedule(
|
|
||||||
build,
|
|
||||||
_wbMeshAdapter.MeshManager!),
|
|
||||||
removeEnvCells: _envCellRenderer.RemoveLandblock);
|
|
||||||
var landblockPhysicsPublisher =
|
|
||||||
new AcDream.App.Streaming.LandblockPhysicsPublisher(
|
|
||||||
_physicsEngine,
|
|
||||||
_heightTable!);
|
|
||||||
var landblockStaticPresentationPublisher =
|
|
||||||
new AcDream.App.Streaming.LandblockStaticPresentationPublisher(
|
|
||||||
_lightingSink!,
|
|
||||||
_translucencyFades,
|
|
||||||
_worldGameState,
|
|
||||||
_worldEvents);
|
|
||||||
var landblockRetirementOwner =
|
|
||||||
new AcDream.App.Streaming.LandblockPresentationRetirementOwner(
|
|
||||||
landblockRenderPublisher,
|
|
||||||
landblockPhysicsPublisher,
|
|
||||||
landblockStaticPresentationPublisher,
|
|
||||||
_lightingSink!,
|
|
||||||
_translucencyFades);
|
|
||||||
_landblockPresentationPipeline =
|
|
||||||
new AcDream.App.Streaming.LandblockPresentationPipeline(
|
|
||||||
landblockRenderPublisher,
|
|
||||||
landblockPhysicsPublisher,
|
|
||||||
landblockStaticPresentationPublisher,
|
|
||||||
_worldState,
|
|
||||||
landblockRetirementOwner,
|
|
||||||
onLandblockLoaded: loadedLandblockId =>
|
|
||||||
_liveEntityHydration!.OnLandblockLoaded(loadedLandblockId),
|
|
||||||
ensureEnvCellMeshes:
|
|
||||||
landblockRenderPublisher.PrepareAfterRenderPins);
|
|
||||||
|
|
||||||
_clipFrame ??= ClipFrame.NoClip();
|
|
||||||
// T1: invisible portal depth writes (seal/punch) — retail
|
|
||||||
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
|
|
||||||
_portalDepthMask = new AcDream.App.Rendering.PortalDepthMaskRenderer(_gl);
|
|
||||||
_portalTunnelFallback.AcquirePrepared(
|
|
||||||
() => AcDream.App.Rendering.PortalTunnelPresentation.CreateRequired(
|
|
||||||
_gl,
|
|
||||||
_dats!,
|
|
||||||
_animLoader!,
|
|
||||||
_hookRouter,
|
|
||||||
_wbDrawDispatcher,
|
|
||||||
_sceneLightingUbo!,
|
|
||||||
_wbMeshAdapter!),
|
|
||||||
static portalTunnel => portalTunnel.PrepareResources());
|
|
||||||
// Publish the presentation before touching the mesh-reference
|
|
||||||
// backend. Partial acquisition remains reachable by staged
|
|
||||||
// shutdown instead of becoming constructor-local leaked state.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
|
||||||
// with depth writes off + far plane 1e6 so celestial meshes
|
|
||||||
// never clip. Shares the TextureCache with the static pipeline.
|
|
||||||
var skyShader = _renderResourceLifetime.AcquireSkyShader(
|
|
||||||
() => new Shader(
|
|
||||||
_gl,
|
|
||||||
Path.Combine(shadersDir, "sky.vert"),
|
|
||||||
Path.Combine(shadersDir, "sky.frag")));
|
|
||||||
_skyRenderer = new AcDream.App.Rendering.Sky.SkyRenderer(
|
|
||||||
_gl,
|
|
||||||
contentEffectsAudio.Dats,
|
|
||||||
skyShader,
|
|
||||||
_textureCache!,
|
|
||||||
_samplerCache!);
|
|
||||||
|
|
||||||
// Phase G.1 particle renderer — renders rain / snow / spell auras
|
|
||||||
// spawned into the shared ParticleSystem. Mode-1/no-degrade GfxObjs
|
|
||||||
// retain their authored mesh; Always2D degrade modes use billboards.
|
|
||||||
// Weather uses AttachLocal emitters so its volume follows the player.
|
|
||||||
_particleRenderer = new ParticleRenderer(
|
|
||||||
_gl,
|
|
||||||
shadersDir,
|
|
||||||
contentEffectsAudio.ParticleSystem,
|
|
||||||
_textureCache,
|
|
||||||
contentEffectsAudio.Dats,
|
|
||||||
_wbMeshAdapter,
|
|
||||||
_retailAlphaQueue);
|
|
||||||
|
|
||||||
AcDream.App.Rendering.IRenderFrameResourceDiagnosticsSource? resourceDiagnostics =
|
|
||||||
_options.UiProbeDump
|
|
||||||
? new AcDream.App.Rendering.RuntimeRenderFrameResourceDiagnosticsSource(
|
|
||||||
_particleSystem,
|
|
||||||
_particleSink,
|
|
||||||
_wbDrawDispatcher,
|
|
||||||
_envCellRenderer,
|
|
||||||
_particleRenderer,
|
|
||||||
_uiHost?.TextRenderer,
|
|
||||||
_portalDepthMask,
|
|
||||||
_clipFrame,
|
|
||||||
_terrain,
|
|
||||||
_sceneLightingUbo,
|
|
||||||
_wbMeshAdapter,
|
|
||||||
_textureCache)
|
|
||||||
: null;
|
|
||||||
_renderFrameDiagnostics =
|
|
||||||
new AcDream.App.Rendering.RenderFrameDiagnosticsController(
|
|
||||||
new AcDream.App.Rendering.RuntimeRenderFrameTitleFactsSource(
|
|
||||||
_worldState,
|
|
||||||
_animatedEntities,
|
_animatedEntities,
|
||||||
WorldTime),
|
_animationDiagnostics,
|
||||||
new AcDream.App.Rendering.SilkRenderFrameTitleSink(_window!),
|
_classificationCache,
|
||||||
|
_translucencyFades,
|
||||||
|
_retailAlphaQueue,
|
||||||
|
_cellVisibility,
|
||||||
|
_liveWorldOrigin,
|
||||||
|
_localPlayerIdentity,
|
||||||
|
_playerControllerSlot,
|
||||||
|
_pointerPosition,
|
||||||
|
_playerApproachCompletions,
|
||||||
|
_renderResourceLifetime,
|
||||||
|
_portalTunnelFallback,
|
||||||
|
_hookRouter,
|
||||||
_renderDiagnosticLog,
|
_renderDiagnosticLog,
|
||||||
_options.UiProbeDump,
|
WorldTime,
|
||||||
resourceDiagnostics);
|
settingsDevTools.DevTools?.LateBindings.WorldEntities,
|
||||||
_devToolsComposition?.LateBindings.FrameDiagnostics.Bind(
|
settingsDevTools.DevTools?.LateBindings.FrameDiagnostics,
|
||||||
_renderFrameDiagnostics);
|
_uiFrameDiagnostics,
|
||||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
compositionToast),
|
||||||
"render-frame diagnostics",
|
this).Compose(
|
||||||
_uiFrameDiagnostics.BindOwned(_renderFrameDiagnostics));
|
hostInputCamera,
|
||||||
|
contentEffectsAudio,
|
||||||
|
worldRender,
|
||||||
|
interactionUi);
|
||||||
|
|
||||||
// Apply radii from the same immutable quality snapshot used for the
|
// Apply radii from the same immutable quality snapshot used for the
|
||||||
// window's MSAA, terrain anisotropy, and dispatcher A2C setup.
|
// window's MSAA, terrain anisotropy, and dispatcher A2C setup.
|
||||||
|
|
@ -1792,7 +1384,7 @@ public sealed class GameWindow :
|
||||||
_liveSessionController);
|
_liveSessionController);
|
||||||
var liveEntityTeardown =
|
var liveEntityTeardown =
|
||||||
new AcDream.App.World.LiveEntityRuntimeTeardownController(
|
new AcDream.App.World.LiveEntityRuntimeTeardownController(
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_liveEntityPresentation!,
|
_liveEntityPresentation!,
|
||||||
_entityEffects!,
|
_entityEffects!,
|
||||||
_remoteTeleportController!,
|
_remoteTeleportController!,
|
||||||
|
|
@ -1807,23 +1399,23 @@ public sealed class GameWindow :
|
||||||
_liveEntityLights!,
|
_liveEntityLights!,
|
||||||
_classificationCache,
|
_classificationCache,
|
||||||
_localPlayerIdentity);
|
_localPlayerIdentity);
|
||||||
liveEntityComponentLifecycle.Bind(liveEntityTeardown);
|
livePresentation.ComponentLifecycle.Bind(liveEntityTeardown);
|
||||||
var liveEntityDeletion =
|
var liveEntityDeletion =
|
||||||
new AcDream.App.World.LiveEntityDeletionController(
|
new AcDream.App.World.LiveEntityDeletionController(
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
Objects,
|
Objects,
|
||||||
liveEntityTeardown,
|
liveEntityTeardown,
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
||||||
_liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController(
|
_liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController(
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
Objects,
|
Objects,
|
||||||
_datLock,
|
_datLock,
|
||||||
projectionMaterializer,
|
projectionMaterializer,
|
||||||
new AcDream.App.World.LiveEntityRelationshipProjection(
|
new AcDream.App.World.LiveEntityRelationshipProjection(
|
||||||
_equippedChildRenderer),
|
livePresentation.EquippedChildren),
|
||||||
new AcDream.App.World.LiveEntityReadyPublisher(
|
new AcDream.App.World.LiveEntityReadyPublisher(
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_entityEffects!,
|
_entityEffects!,
|
||||||
_liveEntityPresentation!),
|
_liveEntityPresentation!),
|
||||||
originCoordinator,
|
originCoordinator,
|
||||||
|
|
@ -1832,9 +1424,12 @@ public sealed class GameWindow :
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
liveEntityDeletion,
|
liveEntityDeletion,
|
||||||
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
||||||
|
livePresentation.RuntimeBindings.Adopt(
|
||||||
|
"landblock-loaded hydration",
|
||||||
|
livePresentation.LandblockLoaded.Bind(_liveEntityHydration));
|
||||||
_liveEntityNetworkUpdates =
|
_liveEntityNetworkUpdates =
|
||||||
new AcDream.App.Physics.LiveEntityNetworkUpdateController(
|
new AcDream.App.Physics.LiveEntityNetworkUpdateController(
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
Objects,
|
Objects,
|
||||||
_liveEntityHydration,
|
_liveEntityHydration,
|
||||||
_entityEffects!,
|
_entityEffects!,
|
||||||
|
|
@ -1849,7 +1444,7 @@ public sealed class GameWindow :
|
||||||
_remoteMovementObservations,
|
_remoteMovementObservations,
|
||||||
_remotePhysicsUpdater,
|
_remotePhysicsUpdater,
|
||||||
_remoteInboundMotion,
|
_remoteInboundMotion,
|
||||||
liveEntityMotionRuntime!,
|
livePresentation.MotionRuntime,
|
||||||
_physicsEngine,
|
_physicsEngine,
|
||||||
_dats!,
|
_dats!,
|
||||||
_animLoader!,
|
_animLoader!,
|
||||||
|
|
@ -1865,7 +1460,7 @@ public sealed class GameWindow :
|
||||||
localPhysicsTimestamps.Publish,
|
localPhysicsTimestamps.Publish,
|
||||||
_movementTruthDiagnostics);
|
_movementTruthDiagnostics);
|
||||||
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
|
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
liveEntityDeletion);
|
liveEntityDeletion);
|
||||||
_liveEntitySessionEvents = new AcDream.App.Net.LiveEntitySessionController(
|
_liveEntitySessionEvents = new AcDream.App.Net.LiveEntitySessionController(
|
||||||
|
|
@ -1874,9 +1469,10 @@ public sealed class GameWindow :
|
||||||
_liveEntityNetworkUpdates,
|
_liveEntityNetworkUpdates,
|
||||||
_localPlayerTeleportSink,
|
_localPlayerTeleportSink,
|
||||||
_entityEffects!);
|
_entityEffects!);
|
||||||
parentAcceptance!.Bind(_liveEntityHydration.TryAcceptParentForProjection);
|
livePresentation.ParentAcceptance.Bind(
|
||||||
|
_liveEntityHydration.TryAcceptParentForProjection);
|
||||||
networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
|
networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
|
||||||
_equippedChildRenderer.EntityReady += candidate =>
|
livePresentation.EquippedChildren.EntityReady += candidate =>
|
||||||
_liveEntityHydration.OnEntityReady(candidate);
|
_liveEntityHydration.OnEntityReady(candidate);
|
||||||
_liveEntityHydration.AppearanceApplied += guid =>
|
_liveEntityHydration.AppearanceApplied += guid =>
|
||||||
{
|
{
|
||||||
|
|
@ -1940,7 +1536,7 @@ public sealed class GameWindow :
|
||||||
_streamingController!,
|
_streamingController!,
|
||||||
new AcDream.App.Streaming.LiveProjectionRescueRebucketter(
|
new AcDream.App.Streaming.LiveProjectionRescueRebucketter(
|
||||||
_worldState,
|
_worldState,
|
||||||
_liveEntities));
|
livePresentation.LiveEntities));
|
||||||
var liveEffectFrame = new AcDream.App.Update.LiveEffectFrameController(
|
var liveEffectFrame = new AcDream.App.Update.LiveEffectFrameController(
|
||||||
_translucencyFades,
|
_translucencyFades,
|
||||||
_animationHookFrames!,
|
_animationHookFrames!,
|
||||||
|
|
@ -1961,7 +1557,7 @@ public sealed class GameWindow :
|
||||||
_liveEntityLights!);
|
_liveEntityLights!);
|
||||||
_localPlayerAnimation =
|
_localPlayerAnimation =
|
||||||
new AcDream.App.Input.LocalPlayerAnimationController(
|
new AcDream.App.Input.LocalPlayerAnimationController(
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
_animatedEntities,
|
_animatedEntities,
|
||||||
_animationPresenter,
|
_animationPresenter,
|
||||||
|
|
@ -1969,14 +1565,14 @@ public sealed class GameWindow :
|
||||||
_localPlayerShadowSynchronizer =
|
_localPlayerShadowSynchronizer =
|
||||||
new AcDream.App.Physics.LocalPlayerShadowSynchronizer(
|
new AcDream.App.Physics.LocalPlayerShadowSynchronizer(
|
||||||
_physicsEngine,
|
_physicsEngine,
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
_liveWorldOrigin,
|
_liveWorldOrigin,
|
||||||
_localPlayerShadow);
|
_localPlayerShadow);
|
||||||
var localPlayerProjection =
|
var localPlayerProjection =
|
||||||
new AcDream.App.Input.LocalPlayerProjectionController(
|
new AcDream.App.Input.LocalPlayerProjectionController(
|
||||||
new AcDream.App.Input.LiveLocalPlayerProjectionRuntime(
|
new AcDream.App.Input.LiveLocalPlayerProjectionRuntime(
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
_liveWorldOrigin,
|
_liveWorldOrigin,
|
||||||
_localPlayerShadowSynchronizer));
|
_localPlayerShadowSynchronizer));
|
||||||
|
|
@ -1988,7 +1584,7 @@ public sealed class GameWindow :
|
||||||
_chaseCameraInput,
|
_chaseCameraInput,
|
||||||
_movementInput,
|
_movementInput,
|
||||||
_inputCapture,
|
_inputCapture,
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
_playerHostSlot,
|
_playerHostSlot,
|
||||||
localPlayerProjection,
|
localPlayerProjection,
|
||||||
|
|
@ -2002,11 +1598,11 @@ public sealed class GameWindow :
|
||||||
_inboundEntityEvents,
|
_inboundEntityEvents,
|
||||||
localPlayerFrame,
|
localPlayerFrame,
|
||||||
_selectionInteractions,
|
_selectionInteractions,
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
_liveWorldOrigin,
|
_liveWorldOrigin,
|
||||||
_liveAnimationScheduler,
|
_liveAnimationScheduler,
|
||||||
_staticAnimationScheduler,
|
livePresentation.StaticAnimationScheduler,
|
||||||
_animationPresenter,
|
_animationPresenter,
|
||||||
_animatedEntities,
|
_animatedEntities,
|
||||||
_equippedChildRenderer!,
|
_equippedChildRenderer!,
|
||||||
|
|
@ -2018,7 +1614,7 @@ public sealed class GameWindow :
|
||||||
_chaseCameraInput,
|
_chaseCameraInput,
|
||||||
_cameraController!,
|
_cameraController!,
|
||||||
_physicsEngine,
|
_physicsEngine,
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
_liveWorldOrigin,
|
_liveWorldOrigin,
|
||||||
_liveEntityMotionBindings,
|
_liveEntityMotionBindings,
|
||||||
|
|
@ -2040,7 +1636,7 @@ public sealed class GameWindow :
|
||||||
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
|
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
|
||||||
new AcDream.App.Input.LivePlayerModeAutoEntryContext(
|
new AcDream.App.Input.LivePlayerModeAutoEntryContext(
|
||||||
_liveSessionController,
|
_liveSessionController,
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
_worldReveal
|
_worldReveal
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
|
|
@ -2051,7 +1647,7 @@ public sealed class GameWindow :
|
||||||
_localPlayerTeleport = _portalTunnelFallback.Transfer(
|
_localPlayerTeleport = _portalTunnelFallback.Transfer(
|
||||||
portalTunnel => new AcDream.App.Streaming.LocalPlayerTeleportController(
|
portalTunnel => new AcDream.App.Streaming.LocalPlayerTeleportController(
|
||||||
new AcDream.App.Streaming.LiveLocalPlayerTeleportAuthority(
|
new AcDream.App.Streaming.LiveLocalPlayerTeleportAuthority(
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_localPlayerIdentity),
|
_localPlayerIdentity),
|
||||||
_gameplayInputFrame,
|
_gameplayInputFrame,
|
||||||
_playerModeController,
|
_playerModeController,
|
||||||
|
|
@ -2063,7 +1659,7 @@ public sealed class GameWindow :
|
||||||
_worldReveal,
|
_worldReveal,
|
||||||
new AcDream.App.Streaming.LocalPlayerTeleportPlacement(
|
new AcDream.App.Streaming.LocalPlayerTeleportPlacement(
|
||||||
_physicsEngine,
|
_physicsEngine,
|
||||||
_liveEntities,
|
livePresentation.LiveEntities,
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
_playerControllerSlot,
|
_playerControllerSlot,
|
||||||
_playerHostSlot,
|
_playerHostSlot,
|
||||||
|
|
@ -2323,7 +1919,7 @@ public sealed class GameWindow :
|
||||||
_worldSelectionQuery!));
|
_worldSelectionQuery!));
|
||||||
var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
|
var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
|
||||||
new AcDream.App.Update.LiveEntityTeardownFramePhase(
|
new AcDream.App.Update.LiveEntityTeardownFramePhase(
|
||||||
_liveEntities),
|
livePresentation.LiveEntities),
|
||||||
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
|
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
|
||||||
_updateFrameClock,
|
_updateFrameClock,
|
||||||
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
|
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
|
||||||
|
|
@ -3039,6 +2635,15 @@ public sealed class GameWindow :
|
||||||
]),
|
]),
|
||||||
new ResourceShutdownStage("effect dispatch edges",
|
new ResourceShutdownStage("effect dispatch edges",
|
||||||
[
|
[
|
||||||
|
new("live-presentation bindings", () =>
|
||||||
|
{
|
||||||
|
LivePresentationRuntimeBindings? bindings =
|
||||||
|
_livePresentationBindings;
|
||||||
|
if (bindings is null)
|
||||||
|
return;
|
||||||
|
bindings.Dispose();
|
||||||
|
_livePresentationBindings = null;
|
||||||
|
}),
|
||||||
new("entity-effect advance source", () =>
|
new("entity-effect advance source", () =>
|
||||||
{
|
{
|
||||||
if (_entityEffects is { } effects)
|
if (_entityEffects is { } effects)
|
||||||
|
|
@ -3232,15 +2837,4 @@ public sealed class GameWindow :
|
||||||
_window = null;
|
_window = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fallback <see cref="AcDream.Core.Physics.IAnimationLoader"/> for the
|
|
||||||
/// <see cref="AcDream.App.Rendering.Wb.EntitySpawnAdapter"/> sequencer
|
|
||||||
/// factory when neither <c>_dats</c> nor the entity's setup is available.
|
|
||||||
/// Returns null for all animation lookups so the sequencer silently has
|
|
||||||
/// no data (same behaviour as a new empty Setup).
|
|
||||||
/// </summary>
|
|
||||||
private sealed class NullAnimLoader : AcDream.Core.Physics.IAnimationLoader
|
|
||||||
{
|
|
||||||
public DatReaderWriter.DBObjs.Animation? LoadAnimation(uint id) => null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ internal sealed class DeferredEntityEffectAdvanceSource : IEntityEffectAdvanceSo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(IEntityEffectAdvanceSource target)
|
||||||
|
{
|
||||||
|
Bind(target);
|
||||||
|
return new Binding(this, target);
|
||||||
|
}
|
||||||
|
|
||||||
public void Unbind(IEntityEffectAdvanceSource target)
|
public void Unbind(IEntityEffectAdvanceSource target)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(target);
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
|
@ -60,4 +66,21 @@ internal sealed class DeferredEntityEffectAdvanceSource : IEntityEffectAdvanceSo
|
||||||
|| _target?.CanAdvanceOwner(ownerLocalId) != false;
|
|| _target?.CanAdvanceOwner(ownerLocalId) != false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private DeferredEntityEffectAdvanceSource? _owner;
|
||||||
|
private readonly IEntityEffectAdvanceSource _target;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
DeferredEntityEffectAdvanceSource owner,
|
||||||
|
IEntityEffectAdvanceSource target)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
|
internal interface ILiveEntityLandblockLoadedSink
|
||||||
|
{
|
||||||
|
void OnLandblockLoaded(uint landblockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase-6 bridge for the Phase-7 hydration owner. The presentation pipeline
|
||||||
|
/// can publish a loaded landblock without capturing a future nullable owner.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class DeferredLiveEntityLandblockLoadedSink
|
||||||
|
: ILiveEntityLandblockLoadedSink
|
||||||
|
{
|
||||||
|
private ILiveEntityLandblockLoadedSink? _target;
|
||||||
|
private bool _deactivated;
|
||||||
|
|
||||||
|
public void OnLandblockLoaded(uint landblockId)
|
||||||
|
{
|
||||||
|
if (!_deactivated)
|
||||||
|
_target?.OnLandblockLoaded(landblockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable Bind(ILiveEntityLandblockLoadedSink target)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||||
|
if (_target is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Live-entity landblock hydration is already bound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_target = target;
|
||||||
|
return new Binding(this, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Deactivate()
|
||||||
|
{
|
||||||
|
_deactivated = true;
|
||||||
|
_target = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unbind(ILiveEntityLandblockLoadedSink expected)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_target, expected))
|
||||||
|
_target = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private DeferredLiveEntityLandblockLoadedSink? _owner;
|
||||||
|
private readonly ILiveEntityLandblockLoadedSink _expected;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
DeferredLiveEntityLandblockLoadedSink owner,
|
||||||
|
ILiveEntityLandblockLoadedSink expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -137,7 +137,7 @@ internal interface ILiveEntityWorldOriginCoordinator
|
||||||
/// Retail anchors: <c>SmartBox::HandleCreateObject @ 0x00454C80</c> and
|
/// Retail anchors: <c>SmartBox::HandleCreateObject @ 0x00454C80</c> and
|
||||||
/// <c>ACCObjectMaint::CreateObject @ 0x00558870</c>.
|
/// <c>ACCObjectMaint::CreateObject @ 0x00558870</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class LiveEntityHydrationController
|
internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoadedSink
|
||||||
{
|
{
|
||||||
private readonly LiveEntityRuntime _runtime;
|
private readonly LiveEntityRuntime _runtime;
|
||||||
private readonly ClientObjectTable _objects;
|
private readonly ClientObjectTable _objects;
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,31 @@ internal sealed class LiveEntityRuntimeSlot : ILiveEntityRuntimeSource
|
||||||
throw new InvalidOperationException("The live entity runtime is already bound.");
|
throw new InvalidOperationException("The live entity runtime is already bound.");
|
||||||
Current = runtime;
|
Current = runtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(LiveEntityRuntime runtime)
|
||||||
|
{
|
||||||
|
Bind(runtime);
|
||||||
|
return new Binding(this, runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unbind(LiveEntityRuntime expected)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(Current, expected))
|
||||||
|
Current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private LiveEntityRuntimeSlot? _owner;
|
||||||
|
private readonly LiveEntityRuntime _expected;
|
||||||
|
|
||||||
|
public Binding(LiveEntityRuntimeSlot owner, LiveEntityRuntime expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,36 @@ public sealed class AnimationHookRegistrationSetTests
|
||||||
registrations.Register(new Sink("late")));
|
registrations.Register(new Sink("late")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OwnedRegistrationReleasesExactlyAndRetriesItsOwnFailure()
|
||||||
|
{
|
||||||
|
int adds = 0;
|
||||||
|
int removes = 0;
|
||||||
|
bool fail = true;
|
||||||
|
var sink = new Sink("owned");
|
||||||
|
var registrations = new AnimationHookRegistrationSet(
|
||||||
|
_ => adds++,
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
removes++;
|
||||||
|
if (fail)
|
||||||
|
throw new InvalidOperationException("retry");
|
||||||
|
});
|
||||||
|
|
||||||
|
IDisposable binding = registrations.RegisterOwned(sink);
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
registrations.RegisterOwned(sink));
|
||||||
|
Assert.Throws<InvalidOperationException>(binding.Dispose);
|
||||||
|
fail = false;
|
||||||
|
binding.Dispose();
|
||||||
|
binding.Dispose();
|
||||||
|
registrations.Dispose();
|
||||||
|
|
||||||
|
Assert.Equal(1, adds);
|
||||||
|
Assert.Equal(2, removes);
|
||||||
|
Assert.True(registrations.IsCleanupComplete);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class Sink(string name) : IAnimationHookSink
|
private sealed class Sink(string name) : IAnimationHookSink
|
||||||
{
|
{
|
||||||
public string Name { get; } = name;
|
public string Name { get; } = name;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
using AcDream.App.Composition;
|
||||||
|
using AcDream.App.Physics;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.Net;
|
||||||
|
using AcDream.Core.Physics.Motion;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Composition;
|
||||||
|
|
||||||
|
public sealed class LivePresentationCompositionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RuntimeBindingsReleaseInReverseAndRetryOnlyFailedEdges()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var bindings = new LivePresentationRuntimeBindings();
|
||||||
|
var first = new RetryBinding("first", calls, failures: 0);
|
||||||
|
var second = new RetryBinding("second", calls, failures: 1);
|
||||||
|
bindings.Adopt("first", first);
|
||||||
|
bindings.Adopt("second", second);
|
||||||
|
|
||||||
|
Assert.Throws<AggregateException>(bindings.Dispose);
|
||||||
|
Assert.Equal(["second", "first"], calls);
|
||||||
|
|
||||||
|
bindings.Dispose();
|
||||||
|
bindings.Dispose();
|
||||||
|
|
||||||
|
Assert.Equal(["second", "first", "second"], calls);
|
||||||
|
Assert.Throws<ObjectDisposedException>(() =>
|
||||||
|
bindings.Adopt("late", new RetryBinding("late", calls, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanonicalRuntimeSlotUsesExactOwnerBinding()
|
||||||
|
{
|
||||||
|
var slot = new LiveEntityRuntimeSlot();
|
||||||
|
LiveEntityRuntime first = Runtime();
|
||||||
|
LiveEntityRuntime second = Runtime();
|
||||||
|
|
||||||
|
IDisposable stale = slot.BindOwned(first);
|
||||||
|
Assert.Same(first, slot.Current);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => slot.BindOwned(second));
|
||||||
|
|
||||||
|
stale.Dispose();
|
||||||
|
IDisposable current = slot.BindOwned(second);
|
||||||
|
stale.Dispose();
|
||||||
|
Assert.Same(second, slot.Current);
|
||||||
|
current.Dispose();
|
||||||
|
Assert.Null(slot.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MotionBindingCannotBeClearedByAStaleOwner()
|
||||||
|
{
|
||||||
|
var source = new DeferredLiveEntityMotionRuntimeBindings();
|
||||||
|
var first = new MotionRuntime(1f);
|
||||||
|
var second = new MotionRuntime(2f);
|
||||||
|
var entity = new WorldEntity
|
||||||
|
{
|
||||||
|
Id = 1u,
|
||||||
|
SourceGfxObjOrSetupId = 1u,
|
||||||
|
Position = System.Numerics.Vector3.Zero,
|
||||||
|
Rotation = System.Numerics.Quaternion.Identity,
|
||||||
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
IDisposable stale = source.BindOwned(first);
|
||||||
|
Assert.Equal(1f, source.GetSetupCylinder(1u, entity).Radius);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => source.BindOwned(second));
|
||||||
|
|
||||||
|
stale.Dispose();
|
||||||
|
IDisposable current = source.BindOwned(second);
|
||||||
|
stale.Dispose();
|
||||||
|
Assert.Equal(2f, source.GetSetupCylinder(1u, entity).Radius);
|
||||||
|
current.Dispose();
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
source.GetSetupCylinder(1u, entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LandblockLoadedBridgeIsInertUntilBoundAndDeactivationIsTerminal()
|
||||||
|
{
|
||||||
|
var source = new DeferredLiveEntityLandblockLoadedSink();
|
||||||
|
var first = new LandblockSink();
|
||||||
|
var second = new LandblockSink();
|
||||||
|
|
||||||
|
source.OnLandblockLoaded(1u);
|
||||||
|
IDisposable stale = source.Bind(first);
|
||||||
|
source.OnLandblockLoaded(2u);
|
||||||
|
Assert.Equal([2u], first.Landblocks);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => source.Bind(second));
|
||||||
|
|
||||||
|
stale.Dispose();
|
||||||
|
IDisposable current = source.Bind(second);
|
||||||
|
stale.Dispose();
|
||||||
|
source.OnLandblockLoaded(3u);
|
||||||
|
Assert.Equal([3u], second.Landblocks);
|
||||||
|
|
||||||
|
source.Deactivate();
|
||||||
|
current.Dispose();
|
||||||
|
source.OnLandblockLoaded(4u);
|
||||||
|
Assert.Equal([3u], second.Landblocks);
|
||||||
|
Assert.Throws<ObjectDisposedException>(() => source.Bind(first));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GameWindowUsesLivePhaseAndContainsNoPhaseSixConstructionBody()
|
||||||
|
{
|
||||||
|
string root = FindRepoRoot();
|
||||||
|
string window = File.ReadAllText(Path.Combine(
|
||||||
|
root,
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Rendering",
|
||||||
|
"GameWindow.cs"));
|
||||||
|
string phase = File.ReadAllText(Path.Combine(
|
||||||
|
root,
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"LivePresentationComposition.cs"));
|
||||||
|
|
||||||
|
Assert.Contains("new LivePresentationCompositionPhase(", window);
|
||||||
|
Assert.DoesNotContain("new AcDream.App.World.LiveEntityRuntime(", window);
|
||||||
|
Assert.DoesNotContain("new AcDream.App.Rendering.Wb.WbDrawDispatcher(", window);
|
||||||
|
Assert.DoesNotContain("new AcDream.App.Streaming.LandblockRenderPublisher(", window);
|
||||||
|
Assert.DoesNotContain("_portalTunnelFallback.AcquirePrepared(", window);
|
||||||
|
Assert.Contains("new LiveEntityRuntime(", phase);
|
||||||
|
Assert.Contains("new WbDrawDispatcher(", phase);
|
||||||
|
Assert.Contains("new LandblockRenderPublisher(", phase);
|
||||||
|
Assert.Contains("d.PortalTunnelFallback.AcquirePrepared(", phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LiveEntityRuntime Runtime() => new(
|
||||||
|
new GpuWorldState(),
|
||||||
|
new DelegateLiveEntityResourceLifecycle(static _ => { }, static _ => { }));
|
||||||
|
|
||||||
|
private sealed class MotionRuntime(float radius)
|
||||||
|
: ILiveEntityMotionRuntimeBindings
|
||||||
|
{
|
||||||
|
public (float Radius, float Height) GetSetupCylinder(
|
||||||
|
uint serverGuid,
|
||||||
|
WorldEntity entity) => (radius, 1f);
|
||||||
|
|
||||||
|
public bool RouteServerMoveTo(
|
||||||
|
MovementManager movement,
|
||||||
|
uint cellId,
|
||||||
|
WorldSession.EntityMotionUpdate update) => false;
|
||||||
|
|
||||||
|
public void StickToObjectFromWire(
|
||||||
|
IPhysicsObjHost? host,
|
||||||
|
uint targetGuid)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearTargetForHiddenEntity(uint serverGuid)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPhysicsObjHost? ResolvePhysicsHost(
|
||||||
|
uint serverGuid) => null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LandblockSink : ILiveEntityLandblockLoadedSink
|
||||||
|
{
|
||||||
|
public List<uint> Landblocks { get; } = [];
|
||||||
|
public void OnLandblockLoaded(uint landblockId) => Landblocks.Add(landblockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RetryBinding(
|
||||||
|
string name,
|
||||||
|
List<string> calls,
|
||||||
|
int failures) : IDisposable
|
||||||
|
{
|
||||||
|
private int _failures = failures;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
calls.Add(name);
|
||||||
|
if (_failures > 0)
|
||||||
|
{
|
||||||
|
_failures--;
|
||||||
|
throw new InvalidOperationException("retry");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindRepoRoot()
|
||||||
|
{
|
||||||
|
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||||
|
while (directory is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||||
|
return directory.FullName;
|
||||||
|
directory = directory.Parent;
|
||||||
|
}
|
||||||
|
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -93,7 +93,7 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
foreach (string identifier in removed)
|
foreach (string identifier in removed)
|
||||||
Assert.DoesNotContain(identifier, source, StringComparison.Ordinal);
|
Assert.DoesNotContain(identifier, source, StringComparison.Ordinal);
|
||||||
|
|
||||||
Assert.Contains("new AcDream.App.Rendering.PaperdollFramePresenter(", source);
|
Assert.Contains("new PaperdollFramePresenter(", LivePresentationSource());
|
||||||
string settingsComposition = File.ReadAllText(Path.Combine(
|
string settingsComposition = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
"src",
|
"src",
|
||||||
|
|
@ -193,13 +193,13 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PaperdollComposition_SkipsEitherMissingOptionalUiSurface()
|
public void PaperdollComposition_SkipsEitherMissingOptionalUiSurface()
|
||||||
{
|
{
|
||||||
string source = GameWindowSource();
|
string source = LivePresentationSource();
|
||||||
|
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
source,
|
||||||
"PaperdollViewportWidget is { } paperdollViewport",
|
"PaperdollViewportWidget is { } viewport",
|
||||||
"InventoryFrame is { } paperdollInventoryFrame",
|
"InventoryFrame is { } inventoryFrame",
|
||||||
"new AcDream.App.Rendering.PaperdollFramePresenter(");
|
"new PaperdollFramePresenter(");
|
||||||
Assert.DoesNotContain("Paperdoll inventory frame is required.", source);
|
Assert.DoesNotContain("Paperdoll inventory frame is required.", source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,6 +223,13 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
"Rendering",
|
"Rendering",
|
||||||
"GameWindow.cs"));
|
"GameWindow.cs"));
|
||||||
|
|
||||||
|
private static string LivePresentationSource() => File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"LivePresentationComposition.cs"));
|
||||||
|
|
||||||
private static string Source(string fileName) => File.ReadAllText(Path.Combine(
|
private static string Source(string fileName) => File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
"src",
|
"src",
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,9 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"InteractionRetainedUiResult interactionUi =",
|
"InteractionRetainedUiResult interactionUi =",
|
||||||
"new InteractionRetainedUiCompositionPhase(",
|
"new InteractionRetainedUiCompositionPhase(",
|
||||||
"this).Compose(",
|
"this).Compose(",
|
||||||
"_liveEntities = new AcDream.App.World.LiveEntityRuntime(",
|
"LivePresentationResult livePresentation =",
|
||||||
"_selectionInteractions ??= new AcDream.App.Interaction.SelectionInteractionController(",
|
"new LivePresentationCompositionPhase(",
|
||||||
"_retainedUiGameplayBinding =",
|
"this).Compose(",
|
||||||
"AcDream.App.Input.RetainedUiGameplayBinding.Create(",
|
|
||||||
"_retainedUiGameplayBinding.Attach();",
|
|
||||||
"var renderFrameOrchestrator =",
|
"var renderFrameOrchestrator =",
|
||||||
"var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
|
"var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
|
||||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);",
|
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);",
|
||||||
|
|
@ -193,11 +191,11 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"AcDream.App.Input.GameplayInputActionRouter.Create("));
|
"AcDream.App.Input.GameplayInputActionRouter.Create("));
|
||||||
Assert.Equal(1, CountOccurrences(load, "_gameplayInputActions.Attach();"));
|
Assert.Equal(1, CountOccurrences(load, "_gameplayInputActions.Attach();"));
|
||||||
Assert.Equal(1, CountOccurrences(
|
Assert.Equal(1, CountOccurrences(
|
||||||
load,
|
LivePresentationSource(),
|
||||||
"AcDream.App.Input.RetainedUiGameplayBinding.Create("));
|
"RetainedUiGameplayBinding.Create("));
|
||||||
Assert.Equal(1, CountOccurrences(
|
Assert.Equal(1, CountOccurrences(
|
||||||
load,
|
LivePresentationSource(),
|
||||||
"_retainedUiGameplayBinding.Attach();"));
|
"retainedGameplayLease.Resource.Attach();"));
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
shutdown,
|
shutdown,
|
||||||
"_liveCombatModeCommands.Deactivate",
|
"_liveCombatModeCommands.Deactivate",
|
||||||
|
|
@ -377,10 +375,9 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
source,
|
source,
|
||||||
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
||||||
"private void OnFocusChanged(bool focused)");
|
"private void OnFocusChanged(bool focused)");
|
||||||
string dispose = Slice(
|
int disposeStart = source.IndexOf("public void Dispose()", StringComparison.Ordinal);
|
||||||
source,
|
Assert.True(disposeStart >= 0);
|
||||||
"public void Dispose()",
|
string dispose = source[disposeStart..];
|
||||||
"private sealed class NullAnimLoader");
|
|
||||||
|
|
||||||
string[] stages =
|
string[] stages =
|
||||||
[
|
[
|
||||||
|
|
@ -474,14 +471,18 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Composition",
|
"Composition",
|
||||||
"WorldRenderComposition.cs"));
|
"WorldRenderComposition.cs"));
|
||||||
|
string livePhase = LivePresentationSource();
|
||||||
|
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
livePhase,
|
||||||
|
"d.PortalTunnelFallback.AcquirePrepared(",
|
||||||
|
"static tunnel => tunnel.PrepareResources());",
|
||||||
|
"d.RenderResourceLifetime.AcquireSkyShader(",
|
||||||
|
"() => new SkyRenderer(");
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
load,
|
load,
|
||||||
"new WorldRenderCompositionPhase(",
|
"new WorldRenderCompositionPhase(",
|
||||||
"_portalTunnelFallback.AcquirePrepared(",
|
"new LivePresentationCompositionPhase(",
|
||||||
"static portalTunnel => portalTunnel.PrepareResources());",
|
|
||||||
"_renderResourceLifetime.AcquireSkyShader(",
|
|
||||||
"_skyRenderer = new AcDream.App.Rendering.Sky.SkyRenderer(",
|
|
||||||
"_portalTunnelFallback.Transfer(",
|
"_portalTunnelFallback.Transfer(",
|
||||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);");
|
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);");
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
|
|
@ -566,6 +567,13 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"Rendering",
|
"Rendering",
|
||||||
"GameWindow.cs"));
|
"GameWindow.cs"));
|
||||||
|
|
||||||
|
private static string LivePresentationSource() => File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"LivePresentationComposition.cs"));
|
||||||
|
|
||||||
private static string FindRepoRoot()
|
private static string FindRepoRoot()
|
||||||
{
|
{
|
||||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,12 @@ public sealed class LandblockBuildOriginTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Rendering",
|
"Rendering",
|
||||||
"GameWindow.cs"));
|
"GameWindow.cs"));
|
||||||
|
string livePresentationSource = File.ReadAllText(Path.Combine(
|
||||||
|
root,
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"LivePresentationComposition.cs"));
|
||||||
string buildSource = File.ReadAllText(Path.Combine(
|
string buildSource = File.ReadAllText(Path.Combine(
|
||||||
root,
|
root,
|
||||||
"src",
|
"src",
|
||||||
|
|
@ -307,16 +313,16 @@ public sealed class LandblockBuildOriginTests
|
||||||
gameWindowSource,
|
gameWindowSource,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"new AcDream.App.Streaming.LandblockRenderPublisher(",
|
"new LandblockRenderPublisher(",
|
||||||
gameWindowSource,
|
livePresentationSource,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"new AcDream.App.Streaming.LandblockPhysicsPublisher(",
|
"new LandblockPhysicsPublisher(",
|
||||||
gameWindowSource,
|
livePresentationSource,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"new AcDream.App.Streaming.LandblockStaticPresentationPublisher(",
|
"new LandblockStaticPresentationPublisher(",
|
||||||
gameWindowSource,
|
livePresentationSource,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("applyTerrain:", gameWindowSource, StringComparison.Ordinal);
|
Assert.DoesNotContain("applyTerrain:", gameWindowSource, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("demoteNearLayer:", gameWindowSource, StringComparison.Ordinal);
|
Assert.DoesNotContain("demoteNearLayer:", gameWindowSource, StringComparison.Ordinal);
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,12 @@ public sealed class GameWindowLiveEntityCompositionTests
|
||||||
|
|
||||||
string source = File.ReadAllText(Path.Combine(
|
string source = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs"));
|
FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs"));
|
||||||
|
string livePresentation = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"LivePresentationComposition.cs"));
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
"new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(() =>",
|
"new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(() =>",
|
||||||
source,
|
source,
|
||||||
|
|
@ -166,7 +172,7 @@ public sealed class GameWindowLiveEntityCompositionTests
|
||||||
"new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(() =>",
|
"new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(() =>",
|
||||||
source,
|
source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("new AcDream.App.Physics.DatProjectileSetupResolver", source);
|
Assert.Contains("new DatProjectileSetupResolver", livePresentation);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,12 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Rendering",
|
"Rendering",
|
||||||
"GameWindow.cs"));
|
"GameWindow.cs"));
|
||||||
|
string livePresentation = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"LivePresentationComposition.cs"));
|
||||||
|
|
||||||
Assert.Contains("new AcDream.App.Update.LiveObjectFrameController(", source);
|
Assert.Contains("new AcDream.App.Update.LiveObjectFrameController(", source);
|
||||||
Assert.Contains("new AcDream.App.Update.LiveSpatialPresentationReconciler(", source);
|
Assert.Contains("new AcDream.App.Update.LiveSpatialPresentationReconciler(", source);
|
||||||
|
|
@ -771,6 +777,12 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Rendering",
|
"Rendering",
|
||||||
"GameWindow.cs"));
|
"GameWindow.cs"));
|
||||||
|
string livePresentation = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"LivePresentationComposition.cs"));
|
||||||
Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source,
|
Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("LoginWorldReady", source,
|
Assert.DoesNotContain("LoginWorldReady", source,
|
||||||
|
|
@ -783,9 +795,9 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("OnPlayScriptReceived", source,
|
Assert.DoesNotContain("OnPlayScriptReceived", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("_liveWorldOrigin.GetCenter", source,
|
Assert.Contains("d.WorldOrigin.GetCenter", livePresentation,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("_liveWorldOrigin.CellLocalForSeed", source,
|
Assert.Contains("d.WorldOrigin.CellLocalForSeed", livePresentation,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("_liveEntitySessionEvents.CreateSink()", source,
|
Assert.Contains("_liveEntitySessionEvents.CreateSink()", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue