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
|
||||
|
||||
- **Active structural work:** `GameWindow` decomposition. Slices 1–7 are
|
||||
complete; Slice 8 checkpoints A–G now own native callbacks, live-session
|
||||
composition, world-environment state, reversible device input/raw pointer,
|
||||
physical framebuffer publication, gameplay action priority, typed commands,
|
||||
the retained-root item-drop edge, and two-phase runtime settings outside the
|
||||
shell.
|
||||
- **Active structural work:** `GameWindow` decomposition. Slices 1–7 and Slice
|
||||
8 checkpoints A–H are complete; I.1–I.6 now own the executable ordered
|
||||
startup path through retained UI and live presentation, including the
|
||||
canonical runtime/world, selection/radar, landblock publishers, and
|
||||
portal/sky/particle resources.
|
||||
`GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration
|
||||
handoffs and the class is 3,663 lines / 162 fields / 37 methods. Remaining
|
||||
Slice 8 resource/composition/shutdown
|
||||
handoffs and the class is 2,840 raw lines. I.7–I.9 plus the remaining
|
||||
shutdown/closeout
|
||||
cleanup is active in
|
||||
[`docs/architecture/code-structure.md`](architecture/code-structure.md).
|
||||
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
|
||||
modern-renderer foundation, immutable terrain-worker inputs, and WB/texture/
|
||||
sampler construction; every Phase-4 GL constructor has retryable prefix
|
||||
ownership. `GameWindow` is 3,522 raw lines after I.5. I.6a moves interaction
|
||||
and retained-UI construction into `InteractionRetainedUiCompositionPhase`;
|
||||
typed exact-owner sources now bridge the later session, selection, radar,
|
||||
view-plane, diagnostics, inventory, and automation owners without callbacks to
|
||||
the window. The old item-use and selection wrapper methods are deleted.
|
||||
`GameWindow` is 3,246 raw lines after I.6a. I.6b–L remain active, with live
|
||||
presentation and landblock-publication composition next. The App gate passes
|
||||
3,401 tests / 3 intentional skips and the complete Release suite passes 7,773
|
||||
tests / 5 intentional skips. The solution retains only the 17 warnings tracked
|
||||
by #228; the I.6a behavior, architecture, and adversarial reviews are clean.
|
||||
ownership. `GameWindow` is 3,522 raw lines after I.5. I.6 moves interaction,
|
||||
retained UI, live presentation, and landblock publication into ordered
|
||||
composition phases. Typed exact-owner sources bridge later session,
|
||||
selection, radar, view-plane, diagnostics, inventory, hydration, and automation
|
||||
owners without callbacks to the window. The old item-use and selection wrapper
|
||||
methods are deleted. `GameWindow` is 2,840 raw lines after I.6. I.7–L remain
|
||||
active, with session, streaming, and hydration composition next. The App gate
|
||||
passes 3,407 tests / 3 intentional skips and the complete Release suite passes
|
||||
7,779 tests / 5 intentional skips. The clean solution build retains only the
|
||||
17 test-project warnings tracked by #228; the I.6 behavior, architecture, and
|
||||
adversarial reviews are clean.
|
||||
|
||||
### 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
|
||||
sole terrain-atlas and sky-shader roots, one retained Host/runtime lease, an
|
||||
atomic update/render frame-root pair, prepared portal fallback/transfer, and
|
||||
checked retryable GL leaf construction and state mutation. `GameWindow` is now
|
||||
3,689 lines / 162 fields / 37 methods. Ordered composition, shutdown, canonical
|
||||
soak snapshots, and closeout checkpoints I–L remain. Checkpoint H passes 61
|
||||
focused ownership tests and 3,236 App tests / 3 intentional skips; the complete
|
||||
Release suite passes 7,606 tests / 5 skips and all three final corrected-diff
|
||||
reviews are clean.
|
||||
checked retryable GL leaf construction and state mutation. Checkpoint I.1–I.6
|
||||
now provide the executable ordered startup pipeline through retained UI and
|
||||
live presentation. The canonical live runtime/world, presentation/effect
|
||||
controllers, selection/radar, landblock publishers, and portal/sky/particle
|
||||
resources have transactional phase ownership and exact-owner late bindings.
|
||||
`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
|
||||
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
|
||||
resource roots, one retained Host/runtime lease, an atomic update/render root
|
||||
pair, a prepared portal fallback/transfer transaction, and checked retryable GL
|
||||
leaf construction. `GameWindow` is 3,689 lines / 162 fields / 37 methods.
|
||||
Checkpoints I–L remain active. Issue #232
|
||||
tracks process-residency variance in the soak without loosening its leak
|
||||
threshold. Checkpoint H passes 61 focused ownership tests and 3,236 App tests /
|
||||
3 skips; the complete Release suite passes 7,606 tests / 5 skips and all three
|
||||
final corrected-diff reviews are clean.
|
||||
leaf construction. Checkpoint I.1–I.6 now provide the executable ordered
|
||||
startup pipeline through retained UI and live presentation. The canonical live
|
||||
runtime/world, presentation/effect controllers, selection/radar, landblock
|
||||
publishers, and portal/sky/particle resources have transactional phase
|
||||
ownership and exact-owner late bindings. `GameWindow` is 2,840 raw lines.
|
||||
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:
|
||||
#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 /
|
||||
5 intentional skips. Production App builds with zero warnings/errors; the
|
||||
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
|
||||
[`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
|
||||
typed dependencies, exact-owner late bindings, and transactional rollback.
|
||||
`GameWindow.cs` is 3,246 raw lines; the App gate passes 3,401 tests / 3
|
||||
intentional skips and the complete Release suite passes 7,773 tests / 5
|
||||
intentional skips. I.6b live presentation and landblock publication is the
|
||||
active cut.
|
||||
Phase 5 now composes interaction and retained UI through typed dependencies,
|
||||
exact-owner late bindings, and transactional rollback. Phase 6 now composes
|
||||
the canonical live runtime/world, presentation/effect controllers,
|
||||
selection/radar, landblock publishers, and portal/sky/particle resources
|
||||
under the same ownership contract. `GameWindow.cs` is 2,840 raw lines; the
|
||||
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.
|
||||
|
||||
## 6. Automated acceptance
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 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`
|
||||
|
||||
|
|
@ -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;
|
||||
- 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
|
||||
|
||||
- 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));
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
_closed = true;
|
||||
|
|
@ -102,4 +127,25 @@ internal sealed class AnimationHookRegistrationSet : IDisposable,
|
|||
"Animation-hook registration cleanup remains incomplete.",
|
||||
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;
|
||||
}
|
||||
|
||||
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 =>
|
||||
_target ?? throw new InvalidOperationException(
|
||||
"Live entity motion runtime bindings were used before binding.");
|
||||
|
|
@ -61,4 +73,21 @@ internal sealed class DeferredLiveEntityMotionRuntimeBindings
|
|||
|
||||
public IPhysicsObjHost? ResolvePhysicsHost(uint 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;
|
||||
}
|
||||
|
||||
public IDisposable BindOwned(GpuWorldState target)
|
||||
{
|
||||
Bind(target);
|
||||
return new Binding(this, target);
|
||||
}
|
||||
|
||||
public void Unbind(GpuWorldState target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
|
|
@ -115,6 +121,23 @@ internal sealed class DeferredCanonicalWorldEntityCountSource
|
|||
_deactivated = true;
|
||||
_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
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ public sealed class GameWindow :
|
|||
IGameWindowContentEffectsAudioPublication,
|
||||
IGameWindowSettingsDevToolsPublication,
|
||||
IGameWindowWorldRenderPublication,
|
||||
IGameWindowInteractionRetainedUiPublication
|
||||
IGameWindowInteractionRetainedUiPublication,
|
||||
IGameWindowLivePresentationPublication
|
||||
{
|
||||
private static double ClientTimerNow() =>
|
||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
|
|
@ -97,6 +98,7 @@ public sealed class GameWindow :
|
|||
_debugVmRenderFacts = new();
|
||||
private AcDream.App.Rendering.RenderFrameDiagnosticsController?
|
||||
_renderFrameDiagnostics;
|
||||
private LivePresentationRuntimeBindings? _livePresentationBindings;
|
||||
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
|
||||
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
|
||||
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>(
|
||||
ref T? destination,
|
||||
T value,
|
||||
|
|
@ -1124,532 +1196,52 @@ public sealed class GameWindow :
|
|||
settingsDevTools,
|
||||
worldRender);
|
||||
|
||||
AcDream.App.World.DeferredLiveEntityParentAcceptance? parentAcceptance = null;
|
||||
// Phase N.4 Task 12: construct LandblockSpawnAdapter under the feature flag
|
||||
// and rebuild _worldState so it threads the adapter in. _worldState starts
|
||||
// as an unadorned GpuWorldState (field initializer); here we replace it with
|
||||
// one that carries the adapter so AddLandblock/RemoveLandblock notify WB.
|
||||
// Phase N.4 Task 17: also construct EntitySpawnAdapter for server-spawned
|
||||
// per-instance content under the same flag.
|
||||
// N.5 mandatory path: spawn adapters + dispatcher always construct.
|
||||
// _wbMeshAdapter, _meshShader, _textureCache, and _bindlessSupport are
|
||||
// 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,
|
||||
LivePresentationResult livePresentation =
|
||||
new LivePresentationCompositionPhase(
|
||||
new LivePresentationDependencies(
|
||||
_options,
|
||||
platform.Graphics,
|
||||
_window!,
|
||||
_datLock,
|
||||
_runtimeSettings,
|
||||
_hostQuiescence,
|
||||
_physicsEngine,
|
||||
new AcDream.App.Physics.DatProjectileSetupResolver(_dats!, _datLock),
|
||||
new EntityRootPosePublisher(_effectPoses),
|
||||
_liveWorldOrigin)
|
||||
{
|
||||
DiagnosticSink = message => Console.Error.WriteLine($"projectile: {message}"),
|
||||
};
|
||||
_liveEntityProjectionWithdrawal =
|
||||
new AcDream.App.World.LiveEntityProjectionWithdrawalController(
|
||||
_liveEntities,
|
||||
_projectileController,
|
||||
_physicsDataCache,
|
||||
_worldGameState,
|
||||
_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,
|
||||
_worldSelectionQuery,
|
||||
itemInteractions,
|
||||
new AcDream.App.Interaction.WorldSessionSelectionInteractionTransport(
|
||||
() => interactionUi.LateBindings.Session.CurrentSession),
|
||||
new AcDream.App.Interaction.PlayerInteractionMovementSink(
|
||||
() => _playerControllerSlot.Controller,
|
||||
_playerApproachCompletions),
|
||||
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,
|
||||
Objects,
|
||||
_liveEntityRuntimeSlot,
|
||||
_liveEntityMotionBindings,
|
||||
_entityEffectAdvance,
|
||||
_effectPoses,
|
||||
_remotePhysicsUpdater,
|
||||
_localPlayerShadow,
|
||||
_animatedEntities,
|
||||
WorldTime),
|
||||
new AcDream.App.Rendering.SilkRenderFrameTitleSink(_window!),
|
||||
_animationDiagnostics,
|
||||
_classificationCache,
|
||||
_translucencyFades,
|
||||
_retailAlphaQueue,
|
||||
_cellVisibility,
|
||||
_liveWorldOrigin,
|
||||
_localPlayerIdentity,
|
||||
_playerControllerSlot,
|
||||
_pointerPosition,
|
||||
_playerApproachCompletions,
|
||||
_renderResourceLifetime,
|
||||
_portalTunnelFallback,
|
||||
_hookRouter,
|
||||
_renderDiagnosticLog,
|
||||
_options.UiProbeDump,
|
||||
resourceDiagnostics);
|
||||
_devToolsComposition?.LateBindings.FrameDiagnostics.Bind(
|
||||
_renderFrameDiagnostics);
|
||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
||||
"render-frame diagnostics",
|
||||
_uiFrameDiagnostics.BindOwned(_renderFrameDiagnostics));
|
||||
WorldTime,
|
||||
settingsDevTools.DevTools?.LateBindings.WorldEntities,
|
||||
settingsDevTools.DevTools?.LateBindings.FrameDiagnostics,
|
||||
_uiFrameDiagnostics,
|
||||
compositionToast),
|
||||
this).Compose(
|
||||
hostInputCamera,
|
||||
contentEffectsAudio,
|
||||
worldRender,
|
||||
interactionUi);
|
||||
|
||||
// Apply radii from the same immutable quality snapshot used for the
|
||||
// window's MSAA, terrain anisotropy, and dispatcher A2C setup.
|
||||
|
|
@ -1792,7 +1384,7 @@ public sealed class GameWindow :
|
|||
_liveSessionController);
|
||||
var liveEntityTeardown =
|
||||
new AcDream.App.World.LiveEntityRuntimeTeardownController(
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_liveEntityPresentation!,
|
||||
_entityEffects!,
|
||||
_remoteTeleportController!,
|
||||
|
|
@ -1807,23 +1399,23 @@ public sealed class GameWindow :
|
|||
_liveEntityLights!,
|
||||
_classificationCache,
|
||||
_localPlayerIdentity);
|
||||
liveEntityComponentLifecycle.Bind(liveEntityTeardown);
|
||||
livePresentation.ComponentLifecycle.Bind(liveEntityTeardown);
|
||||
var liveEntityDeletion =
|
||||
new AcDream.App.World.LiveEntityDeletionController(
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
Objects,
|
||||
liveEntityTeardown,
|
||||
_localPlayerIdentity,
|
||||
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
||||
_liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController(
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
Objects,
|
||||
_datLock,
|
||||
projectionMaterializer,
|
||||
new AcDream.App.World.LiveEntityRelationshipProjection(
|
||||
_equippedChildRenderer),
|
||||
livePresentation.EquippedChildren),
|
||||
new AcDream.App.World.LiveEntityReadyPublisher(
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_entityEffects!,
|
||||
_liveEntityPresentation!),
|
||||
originCoordinator,
|
||||
|
|
@ -1832,9 +1424,12 @@ public sealed class GameWindow :
|
|||
_localPlayerIdentity,
|
||||
liveEntityDeletion,
|
||||
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
||||
livePresentation.RuntimeBindings.Adopt(
|
||||
"landblock-loaded hydration",
|
||||
livePresentation.LandblockLoaded.Bind(_liveEntityHydration));
|
||||
_liveEntityNetworkUpdates =
|
||||
new AcDream.App.Physics.LiveEntityNetworkUpdateController(
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
Objects,
|
||||
_liveEntityHydration,
|
||||
_entityEffects!,
|
||||
|
|
@ -1849,7 +1444,7 @@ public sealed class GameWindow :
|
|||
_remoteMovementObservations,
|
||||
_remotePhysicsUpdater,
|
||||
_remoteInboundMotion,
|
||||
liveEntityMotionRuntime!,
|
||||
livePresentation.MotionRuntime,
|
||||
_physicsEngine,
|
||||
_dats!,
|
||||
_animLoader!,
|
||||
|
|
@ -1865,7 +1460,7 @@ public sealed class GameWindow :
|
|||
localPhysicsTimestamps.Publish,
|
||||
_movementTruthDiagnostics);
|
||||
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_localPlayerIdentity,
|
||||
liveEntityDeletion);
|
||||
_liveEntitySessionEvents = new AcDream.App.Net.LiveEntitySessionController(
|
||||
|
|
@ -1874,9 +1469,10 @@ public sealed class GameWindow :
|
|||
_liveEntityNetworkUpdates,
|
||||
_localPlayerTeleportSink,
|
||||
_entityEffects!);
|
||||
parentAcceptance!.Bind(_liveEntityHydration.TryAcceptParentForProjection);
|
||||
livePresentation.ParentAcceptance.Bind(
|
||||
_liveEntityHydration.TryAcceptParentForProjection);
|
||||
networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
|
||||
_equippedChildRenderer.EntityReady += candidate =>
|
||||
livePresentation.EquippedChildren.EntityReady += candidate =>
|
||||
_liveEntityHydration.OnEntityReady(candidate);
|
||||
_liveEntityHydration.AppearanceApplied += guid =>
|
||||
{
|
||||
|
|
@ -1940,7 +1536,7 @@ public sealed class GameWindow :
|
|||
_streamingController!,
|
||||
new AcDream.App.Streaming.LiveProjectionRescueRebucketter(
|
||||
_worldState,
|
||||
_liveEntities));
|
||||
livePresentation.LiveEntities));
|
||||
var liveEffectFrame = new AcDream.App.Update.LiveEffectFrameController(
|
||||
_translucencyFades,
|
||||
_animationHookFrames!,
|
||||
|
|
@ -1961,7 +1557,7 @@ public sealed class GameWindow :
|
|||
_liveEntityLights!);
|
||||
_localPlayerAnimation =
|
||||
new AcDream.App.Input.LocalPlayerAnimationController(
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_localPlayerIdentity,
|
||||
_animatedEntities,
|
||||
_animationPresenter,
|
||||
|
|
@ -1969,14 +1565,14 @@ public sealed class GameWindow :
|
|||
_localPlayerShadowSynchronizer =
|
||||
new AcDream.App.Physics.LocalPlayerShadowSynchronizer(
|
||||
_physicsEngine,
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_localPlayerIdentity,
|
||||
_liveWorldOrigin,
|
||||
_localPlayerShadow);
|
||||
var localPlayerProjection =
|
||||
new AcDream.App.Input.LocalPlayerProjectionController(
|
||||
new AcDream.App.Input.LiveLocalPlayerProjectionRuntime(
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_localPlayerIdentity,
|
||||
_liveWorldOrigin,
|
||||
_localPlayerShadowSynchronizer));
|
||||
|
|
@ -1988,7 +1584,7 @@ public sealed class GameWindow :
|
|||
_chaseCameraInput,
|
||||
_movementInput,
|
||||
_inputCapture,
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_localPlayerIdentity,
|
||||
_playerHostSlot,
|
||||
localPlayerProjection,
|
||||
|
|
@ -2002,11 +1598,11 @@ public sealed class GameWindow :
|
|||
_inboundEntityEvents,
|
||||
localPlayerFrame,
|
||||
_selectionInteractions,
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_localPlayerIdentity,
|
||||
_liveWorldOrigin,
|
||||
_liveAnimationScheduler,
|
||||
_staticAnimationScheduler,
|
||||
livePresentation.StaticAnimationScheduler,
|
||||
_animationPresenter,
|
||||
_animatedEntities,
|
||||
_equippedChildRenderer!,
|
||||
|
|
@ -2018,7 +1614,7 @@ public sealed class GameWindow :
|
|||
_chaseCameraInput,
|
||||
_cameraController!,
|
||||
_physicsEngine,
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_localPlayerIdentity,
|
||||
_liveWorldOrigin,
|
||||
_liveEntityMotionBindings,
|
||||
|
|
@ -2040,7 +1636,7 @@ public sealed class GameWindow :
|
|||
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
|
||||
new AcDream.App.Input.LivePlayerModeAutoEntryContext(
|
||||
_liveSessionController,
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_localPlayerIdentity,
|
||||
_worldReveal
|
||||
?? throw new InvalidOperationException(
|
||||
|
|
@ -2051,7 +1647,7 @@ public sealed class GameWindow :
|
|||
_localPlayerTeleport = _portalTunnelFallback.Transfer(
|
||||
portalTunnel => new AcDream.App.Streaming.LocalPlayerTeleportController(
|
||||
new AcDream.App.Streaming.LiveLocalPlayerTeleportAuthority(
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_localPlayerIdentity),
|
||||
_gameplayInputFrame,
|
||||
_playerModeController,
|
||||
|
|
@ -2063,7 +1659,7 @@ public sealed class GameWindow :
|
|||
_worldReveal,
|
||||
new AcDream.App.Streaming.LocalPlayerTeleportPlacement(
|
||||
_physicsEngine,
|
||||
_liveEntities,
|
||||
livePresentation.LiveEntities,
|
||||
_localPlayerIdentity,
|
||||
_playerControllerSlot,
|
||||
_playerHostSlot,
|
||||
|
|
@ -2323,7 +1919,7 @@ public sealed class GameWindow :
|
|||
_worldSelectionQuery!));
|
||||
var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
|
||||
new AcDream.App.Update.LiveEntityTeardownFramePhase(
|
||||
_liveEntities),
|
||||
livePresentation.LiveEntities),
|
||||
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
|
||||
_updateFrameClock,
|
||||
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
|
||||
|
|
@ -3039,6 +2635,15 @@ public sealed class GameWindow :
|
|||
]),
|
||||
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", () =>
|
||||
{
|
||||
if (_entityEffects is { } effects)
|
||||
|
|
@ -3232,15 +2837,4 @@ public sealed class GameWindow :
|
|||
_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)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
|
|
@ -60,4 +66,21 @@ internal sealed class DeferredEntityEffectAdvanceSource : IEntityEffectAdvanceSo
|
|||
|| _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
|
||||
/// <c>ACCObjectMaint::CreateObject @ 0x00558870</c>.
|
||||
/// </summary>
|
||||
internal sealed class LiveEntityHydrationController
|
||||
internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoadedSink
|
||||
{
|
||||
private readonly LiveEntityRuntime _runtime;
|
||||
private readonly ClientObjectTable _objects;
|
||||
|
|
|
|||
|
|
@ -21,4 +21,31 @@ internal sealed class LiveEntityRuntimeSlot : ILiveEntityRuntimeSource
|
|||
throw new InvalidOperationException("The live entity runtime is already bound.");
|
||||
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")));
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
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)
|
||||
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(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
|
|
@ -193,13 +193,13 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
[Fact]
|
||||
public void PaperdollComposition_SkipsEitherMissingOptionalUiSurface()
|
||||
{
|
||||
string source = GameWindowSource();
|
||||
string source = LivePresentationSource();
|
||||
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"PaperdollViewportWidget is { } paperdollViewport",
|
||||
"InventoryFrame is { } paperdollInventoryFrame",
|
||||
"new AcDream.App.Rendering.PaperdollFramePresenter(");
|
||||
"PaperdollViewportWidget is { } viewport",
|
||||
"InventoryFrame is { } inventoryFrame",
|
||||
"new PaperdollFramePresenter(");
|
||||
Assert.DoesNotContain("Paperdoll inventory frame is required.", source);
|
||||
}
|
||||
|
||||
|
|
@ -223,6 +223,13 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
"Rendering",
|
||||
"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(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
|
|
|
|||
|
|
@ -61,11 +61,9 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"InteractionRetainedUiResult interactionUi =",
|
||||
"new InteractionRetainedUiCompositionPhase(",
|
||||
"this).Compose(",
|
||||
"_liveEntities = new AcDream.App.World.LiveEntityRuntime(",
|
||||
"_selectionInteractions ??= new AcDream.App.Interaction.SelectionInteractionController(",
|
||||
"_retainedUiGameplayBinding =",
|
||||
"AcDream.App.Input.RetainedUiGameplayBinding.Create(",
|
||||
"_retainedUiGameplayBinding.Attach();",
|
||||
"LivePresentationResult livePresentation =",
|
||||
"new LivePresentationCompositionPhase(",
|
||||
"this).Compose(",
|
||||
"var renderFrameOrchestrator =",
|
||||
"var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
|
||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);",
|
||||
|
|
@ -193,11 +191,11 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"AcDream.App.Input.GameplayInputActionRouter.Create("));
|
||||
Assert.Equal(1, CountOccurrences(load, "_gameplayInputActions.Attach();"));
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
load,
|
||||
"AcDream.App.Input.RetainedUiGameplayBinding.Create("));
|
||||
LivePresentationSource(),
|
||||
"RetainedUiGameplayBinding.Create("));
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
load,
|
||||
"_retainedUiGameplayBinding.Attach();"));
|
||||
LivePresentationSource(),
|
||||
"retainedGameplayLease.Resource.Attach();"));
|
||||
AssertAppearsInOrder(
|
||||
shutdown,
|
||||
"_liveCombatModeCommands.Deactivate",
|
||||
|
|
@ -377,10 +375,9 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
source,
|
||||
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
||||
"private void OnFocusChanged(bool focused)");
|
||||
string dispose = Slice(
|
||||
source,
|
||||
"public void Dispose()",
|
||||
"private sealed class NullAnimLoader");
|
||||
int disposeStart = source.IndexOf("public void Dispose()", StringComparison.Ordinal);
|
||||
Assert.True(disposeStart >= 0);
|
||||
string dispose = source[disposeStart..];
|
||||
|
||||
string[] stages =
|
||||
[
|
||||
|
|
@ -474,14 +471,18 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"AcDream.App",
|
||||
"Composition",
|
||||
"WorldRenderComposition.cs"));
|
||||
string livePhase = LivePresentationSource();
|
||||
|
||||
AssertAppearsInOrder(
|
||||
livePhase,
|
||||
"d.PortalTunnelFallback.AcquirePrepared(",
|
||||
"static tunnel => tunnel.PrepareResources());",
|
||||
"d.RenderResourceLifetime.AcquireSkyShader(",
|
||||
"() => new SkyRenderer(");
|
||||
AssertAppearsInOrder(
|
||||
load,
|
||||
"new WorldRenderCompositionPhase(",
|
||||
"_portalTunnelFallback.AcquirePrepared(",
|
||||
"static portalTunnel => portalTunnel.PrepareResources());",
|
||||
"_renderResourceLifetime.AcquireSkyShader(",
|
||||
"_skyRenderer = new AcDream.App.Rendering.Sky.SkyRenderer(",
|
||||
"new LivePresentationCompositionPhase(",
|
||||
"_portalTunnelFallback.Transfer(",
|
||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);");
|
||||
AssertAppearsInOrder(
|
||||
|
|
@ -566,6 +567,13 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
|
||||
private static string LivePresentationSource() => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
|
|
|
|||
|
|
@ -241,6 +241,12 @@ public sealed class LandblockBuildOriginTests
|
|||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
string livePresentationSource = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
string buildSource = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
|
|
@ -307,16 +313,16 @@ public sealed class LandblockBuildOriginTests
|
|||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new AcDream.App.Streaming.LandblockRenderPublisher(",
|
||||
gameWindowSource,
|
||||
"new LandblockRenderPublisher(",
|
||||
livePresentationSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new AcDream.App.Streaming.LandblockPhysicsPublisher(",
|
||||
gameWindowSource,
|
||||
"new LandblockPhysicsPublisher(",
|
||||
livePresentationSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new AcDream.App.Streaming.LandblockStaticPresentationPublisher(",
|
||||
gameWindowSource,
|
||||
"new LandblockStaticPresentationPublisher(",
|
||||
livePresentationSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("applyTerrain:", gameWindowSource, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("demoteNearLayer:", gameWindowSource, StringComparison.Ordinal);
|
||||
|
|
|
|||
|
|
@ -158,6 +158,12 @@ public sealed class GameWindowLiveEntityCompositionTests
|
|||
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs"));
|
||||
string livePresentation = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
Assert.DoesNotContain(
|
||||
"new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(() =>",
|
||||
source,
|
||||
|
|
@ -166,7 +172,7 @@ public sealed class GameWindowLiveEntityCompositionTests
|
|||
"new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(() =>",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("new AcDream.App.Physics.DatProjectileSetupResolver", source);
|
||||
Assert.Contains("new DatProjectileSetupResolver", livePresentation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -337,6 +337,12 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"AcDream.App",
|
||||
"Rendering",
|
||||
"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.LiveSpatialPresentationReconciler(", source);
|
||||
|
|
@ -771,6 +777,12 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
string livePresentation = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LoginWorldReady", source,
|
||||
|
|
@ -783,9 +795,9 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("OnPlayScriptReceived", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_liveWorldOrigin.GetCenter", source,
|
||||
Assert.Contains("d.WorldOrigin.GetCenter", livePresentation,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_liveWorldOrigin.CellLocalForSeed", source,
|
||||
Assert.Contains("d.WorldOrigin.CellLocalForSeed", livePresentation,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_liveEntitySessionEvents.CreateSink()", source,
|
||||
StringComparison.Ordinal);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue