feat(app): observe canonical placement receipts
This commit is contained in:
parent
378ca95a67
commit
f05ed5c3cd
14 changed files with 545 additions and 31 deletions
|
|
@ -585,7 +585,8 @@ internal sealed class FrameRootCompositionPhase
|
||||||
session.LocalPlayerFrame,
|
session.LocalPlayerFrame,
|
||||||
session.LiveSpatialReconciler,
|
session.LiveSpatialReconciler,
|
||||||
live.WorldAvailability,
|
live.WorldAvailability,
|
||||||
live.RenderSceneShadow?.LiveProjections);
|
live.RenderSceneShadow?.LiveProjections,
|
||||||
|
session.PlacementProjectionRetry);
|
||||||
var cameraFrame = new CameraFrameController(
|
var cameraFrame = new CameraFrameController(
|
||||||
host.CameraController,
|
host.CameraController,
|
||||||
d.InputCapture,
|
d.InputCapture,
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ internal sealed record LivePresentationResult(
|
||||||
GpuWorldState WorldState,
|
GpuWorldState WorldState,
|
||||||
RenderSceneShadowRuntime? RenderSceneShadow,
|
RenderSceneShadowRuntime? RenderSceneShadow,
|
||||||
LiveEntityRuntime LiveEntities,
|
LiveEntityRuntime LiveEntities,
|
||||||
|
RuntimePlacementPresentationSink PlacementProjection,
|
||||||
ProjectileController ProjectileController,
|
ProjectileController ProjectileController,
|
||||||
LiveEntityProjectionWithdrawalController ProjectionWithdrawal,
|
LiveEntityProjectionWithdrawalController ProjectionWithdrawal,
|
||||||
LiveEntityLightController Lights,
|
LiveEntityLightController Lights,
|
||||||
|
|
@ -451,6 +452,35 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
liveEntities,
|
liveEntities,
|
||||||
particleVisibility,
|
particleVisibility,
|
||||||
"particle projection visibility");
|
"particle projection visibility");
|
||||||
|
var placementVisibilitySinks = new List<
|
||||||
|
Action<LiveEntityRecord, bool>>(3)
|
||||||
|
{
|
||||||
|
wbVisibility,
|
||||||
|
};
|
||||||
|
if (liveRenderProjections is not null)
|
||||||
|
{
|
||||||
|
placementVisibilitySinks.Add(
|
||||||
|
liveRenderProjections.OnProjectionVisibilityChanged);
|
||||||
|
}
|
||||||
|
placementVisibilitySinks.Add(particleVisibility);
|
||||||
|
var placementProjection = new RuntimePlacementPresentationSink(
|
||||||
|
liveEntities,
|
||||||
|
worldTransit,
|
||||||
|
d.WorldGameState,
|
||||||
|
d.WorldEvents,
|
||||||
|
d.EffectPoses,
|
||||||
|
d.LocalPlayerShadow,
|
||||||
|
() => d.PlayerIdentity.ServerGuid,
|
||||||
|
guid =>
|
||||||
|
{
|
||||||
|
if (d.Selection.SelectedObjectId == guid)
|
||||||
|
{
|
||||||
|
d.Selection.Clear(
|
||||||
|
SelectionChangeSource.System,
|
||||||
|
SelectionChangeReason.SelectedObjectRemoved);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
placementVisibilitySinks);
|
||||||
Fault(LivePresentationCompositionPoint.ProjectionVisibilityBound);
|
Fault(LivePresentationCompositionPoint.ProjectionVisibilityBound);
|
||||||
|
|
||||||
var projectileController = new ProjectileController(
|
var projectileController = new ProjectileController(
|
||||||
|
|
@ -619,6 +649,7 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
renderSceneShadow,
|
renderSceneShadow,
|
||||||
renderSceneShadowLease,
|
renderSceneShadowLease,
|
||||||
liveEntities,
|
liveEntities,
|
||||||
|
placementProjection,
|
||||||
projectileController,
|
projectileController,
|
||||||
projectionWithdrawal,
|
projectionWithdrawal,
|
||||||
lightsLease,
|
lightsLease,
|
||||||
|
|
@ -668,6 +699,7 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||||
RenderSceneShadowRuntime>? renderSceneShadowLease,
|
RenderSceneShadowRuntime>? renderSceneShadowLease,
|
||||||
LiveEntityRuntime liveEntities,
|
LiveEntityRuntime liveEntities,
|
||||||
|
RuntimePlacementPresentationSink placementProjection,
|
||||||
ProjectileController projectileController,
|
ProjectileController projectileController,
|
||||||
LiveEntityProjectionWithdrawalController projectionWithdrawal,
|
LiveEntityProjectionWithdrawalController projectionWithdrawal,
|
||||||
CompositionAcquisitionScope.CompositionAcquisitionLease<LiveEntityLightController> lightsLease,
|
CompositionAcquisitionScope.CompositionAcquisitionLease<LiveEntityLightController> lightsLease,
|
||||||
|
|
@ -1147,6 +1179,7 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
worldState,
|
worldState,
|
||||||
renderSceneShadow,
|
renderSceneShadow,
|
||||||
liveEntities,
|
liveEntities,
|
||||||
|
placementProjection,
|
||||||
projectileController,
|
projectileController,
|
||||||
projectionWithdrawal,
|
projectionWithdrawal,
|
||||||
lightsLease.Resource,
|
lightsLease.Resource,
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ internal sealed record SessionPlayerResult(
|
||||||
PlayerModeAutoEntry PlayerModeAutoEntry,
|
PlayerModeAutoEntry PlayerModeAutoEntry,
|
||||||
LocalPlayerTeleportController LocalTeleport,
|
LocalPlayerTeleportController LocalTeleport,
|
||||||
LiveSessionHost SessionHost,
|
LiveSessionHost SessionHost,
|
||||||
|
RuntimePlacementProjectionRetrySlot PlacementProjectionRetry,
|
||||||
CurrentGameRuntimeAdapter GameRuntime,
|
CurrentGameRuntimeAdapter GameRuntime,
|
||||||
GameplayInputActionRouter? GameplayActions,
|
GameplayInputActionRouter? GameplayActions,
|
||||||
SessionPlayerRuntimeBindings RuntimeBindings);
|
SessionPlayerRuntimeBindings RuntimeBindings);
|
||||||
|
|
@ -832,6 +833,9 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
|
|
||||||
AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? vitals =
|
AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? vitals =
|
||||||
interaction.RetainedUi?.Vitals;
|
interaction.RetainedUi?.Vitals;
|
||||||
|
var placementProjectionRetry =
|
||||||
|
new RuntimePlacementProjectionRetrySlot(
|
||||||
|
() => d.Runtime.Generation);
|
||||||
var sessionRuntimeFactory = new LiveSessionRuntimeFactory(
|
var sessionRuntimeFactory = new LiveSessionRuntimeFactory(
|
||||||
new LiveSessionPlayerRuntime(
|
new LiveSessionPlayerRuntime(
|
||||||
d.PlayerIdentity,
|
d.PlayerIdentity,
|
||||||
|
|
@ -878,7 +882,9 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
content.AnimationHookFrames,
|
content.AnimationHookFrames,
|
||||||
live.Presentation,
|
live.Presentation,
|
||||||
d.RemoteMovementObservations,
|
d.RemoteMovementObservations,
|
||||||
live.RenderSceneShadow),
|
live.RenderSceneShadow,
|
||||||
|
live.PlacementProjection,
|
||||||
|
placementProjectionRetry),
|
||||||
liveSessionCommands,
|
liveSessionCommands,
|
||||||
d.Log);
|
d.Log);
|
||||||
LiveSessionHost sessionHost = sessionRuntimeFactory.Create(
|
LiveSessionHost sessionHost = sessionRuntimeFactory.Create(
|
||||||
|
|
@ -1015,6 +1021,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
playerModeAutoEntry,
|
playerModeAutoEntry,
|
||||||
teleportLease.Resource,
|
teleportLease.Resource,
|
||||||
sessionHost,
|
sessionHost,
|
||||||
|
placementProjectionRetry,
|
||||||
gameRuntime,
|
gameRuntime,
|
||||||
gameplayActionsLease?.Resource,
|
gameplayActionsLease?.Resource,
|
||||||
bindings);
|
bindings);
|
||||||
|
|
|
||||||
102
src/AcDream.App/Net/GraphicalSessionEventRoute.cs
Normal file
102
src/AcDream.App/Net/GraphicalSessionEventRoute.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
using AcDream.Runtime;
|
||||||
|
using AcDream.Runtime.Physics;
|
||||||
|
using AcDream.Runtime.Session;
|
||||||
|
|
||||||
|
namespace AcDream.App.Net;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the inbound route, canonical placement observer, and update-thread
|
||||||
|
/// retry lease for one exact graphical session generation.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
|
||||||
|
{
|
||||||
|
private readonly ILiveSessionEventRouting _events;
|
||||||
|
private readonly Func<RuntimePlacementProjectionSubscription>
|
||||||
|
_createSubscription;
|
||||||
|
private readonly Func<RuntimeGenerationToken> _generation;
|
||||||
|
private readonly RuntimePlacementProjectionRetrySlot _retries;
|
||||||
|
private RuntimePlacementProjectionSubscription? _subscription;
|
||||||
|
private IDisposable? _retryLease;
|
||||||
|
private bool _attachStarted;
|
||||||
|
private bool _eventsDisposed;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
internal GraphicalSessionEventRoute(
|
||||||
|
ILiveSessionEventRouting events,
|
||||||
|
GameRuntime runtime,
|
||||||
|
IRuntimePlacementProjectionSink placements,
|
||||||
|
RuntimePlacementProjectionRetrySlot retries)
|
||||||
|
: this(
|
||||||
|
events,
|
||||||
|
() => new RuntimePlacementProjectionSubscription(
|
||||||
|
runtime,
|
||||||
|
placements,
|
||||||
|
retryPendingOnSubscribe: false),
|
||||||
|
() => runtime.Generation,
|
||||||
|
retries)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(runtime);
|
||||||
|
ArgumentNullException.ThrowIfNull(placements);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal GraphicalSessionEventRoute(
|
||||||
|
ILiveSessionEventRouting events,
|
||||||
|
Func<RuntimePlacementProjectionSubscription> createSubscription,
|
||||||
|
Func<RuntimeGenerationToken> generation,
|
||||||
|
RuntimePlacementProjectionRetrySlot retries)
|
||||||
|
{
|
||||||
|
_events = events ?? throw new ArgumentNullException(nameof(events));
|
||||||
|
_createSubscription = createSubscription
|
||||||
|
?? throw new ArgumentNullException(nameof(createSubscription));
|
||||||
|
_generation = generation
|
||||||
|
?? throw new ArgumentNullException(nameof(generation));
|
||||||
|
_retries = retries ?? throw new ArgumentNullException(nameof(retries));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Attach()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
if (_attachStarted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_attachStarted = true;
|
||||||
|
_events.Attach();
|
||||||
|
|
||||||
|
RuntimePlacementProjectionSubscription? subscription = null;
|
||||||
|
IDisposable? retryLease = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subscription = _createSubscription();
|
||||||
|
retryLease = _retries.BindOwned(
|
||||||
|
_generation(),
|
||||||
|
subscription.RetryPending);
|
||||||
|
_subscription = subscription;
|
||||||
|
_retryLease = retryLease;
|
||||||
|
_ = subscription.RetryPending();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
retryLease?.Dispose();
|
||||||
|
subscription?.Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Unpublish the frame callback before detaching the observer. A frame
|
||||||
|
// can therefore never retry a retired generation or disposed route.
|
||||||
|
Interlocked.Exchange(ref _retryLease, null)?.Dispose();
|
||||||
|
Interlocked.Exchange(ref _subscription, null)?.Dispose();
|
||||||
|
if (!_eventsDisposed)
|
||||||
|
{
|
||||||
|
_events.Dispose();
|
||||||
|
_eventsDisposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -83,7 +83,9 @@ internal sealed record LiveSessionWorldRuntime(
|
||||||
AnimationHookFrameQueue AnimationHookFrames,
|
AnimationHookFrameQueue AnimationHookFrames,
|
||||||
LiveEntityPresentationController Presentation,
|
LiveEntityPresentationController Presentation,
|
||||||
RemoteMovementObservationTracker RemoteMovementObservations,
|
RemoteMovementObservationTracker RemoteMovementObservations,
|
||||||
RenderSceneShadowRuntime? RenderSceneShadow);
|
RenderSceneShadowRuntime? RenderSceneShadow,
|
||||||
|
RuntimePlacementPresentationSink PlacementProjection,
|
||||||
|
RuntimePlacementProjectionRetrySlot PlacementRetries);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds the exact per-generation route/reset graph for the canonical live
|
/// Builds the exact per-generation route/reset graph for the canonical live
|
||||||
|
|
@ -225,7 +227,7 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
_player.WorldOrigin.Reset();
|
_player.WorldOrigin.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
private LiveSessionEventRouter CreateEventRouter(WorldSession session)
|
private ILiveSessionEventRouting CreateEventRouter(WorldSession session)
|
||||||
{
|
{
|
||||||
SkillTable? skillTable = _world.Dats.Get<SkillTable>(0x0E000004u);
|
SkillTable? skillTable = _world.Dats.Get<SkillTable>(0x0E000004u);
|
||||||
if (_ui.CharacterSheet is not null)
|
if (_ui.CharacterSheet is not null)
|
||||||
|
|
@ -235,7 +237,7 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
CharacterSheetProvider.LoadExperienceTable(_world.Dats, _log);
|
CharacterSheetProvider.LoadExperienceTable(_world.Dats, _log);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LiveSessionEventRouter(
|
var route = new LiveSessionEventRouter(
|
||||||
session,
|
session,
|
||||||
_world.EntitySession.CreateSink(),
|
_world.EntitySession.CreateSink(),
|
||||||
new LiveEnvironmentSessionSink(
|
new LiveEnvironmentSessionSink(
|
||||||
|
|
@ -248,6 +250,11 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
_domain.Communication.TurbineChat,
|
_domain.Communication.TurbineChat,
|
||||||
_domain.Communication.Friends,
|
_domain.Communication.Friends,
|
||||||
_domain.Communication.Squelch));
|
_domain.Communication.Squelch));
|
||||||
|
return new GraphicalSessionEventRoute(
|
||||||
|
route,
|
||||||
|
_domain.Runtime,
|
||||||
|
_world.PlacementProjection,
|
||||||
|
_world.PlacementRetries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private LiveInventorySessionBindings CreateInventoryBindings() => new(
|
private LiveInventorySessionBindings CreateInventoryBindings() => new(
|
||||||
|
|
|
||||||
87
src/AcDream.App/Net/RuntimePlacementProjectionRetrySlot.cs
Normal file
87
src/AcDream.App/Net/RuntimePlacementProjectionRetrySlot.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
using AcDream.Runtime;
|
||||||
|
|
||||||
|
namespace AcDream.App.Net;
|
||||||
|
|
||||||
|
internal interface IRuntimePlacementProjectionRetryPhase
|
||||||
|
{
|
||||||
|
void RetryPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Publishes the retry callback owned by the exact active graphical session
|
||||||
|
/// route. The frame thread may only reach the binding whose Runtime
|
||||||
|
/// generation is still current; disposing an older lease cannot unbind a
|
||||||
|
/// replacement route.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class RuntimePlacementProjectionRetrySlot
|
||||||
|
: IRuntimePlacementProjectionRetryPhase
|
||||||
|
{
|
||||||
|
private sealed record Binding(
|
||||||
|
long Id,
|
||||||
|
RuntimeGenerationToken Generation,
|
||||||
|
Func<bool> Retry);
|
||||||
|
|
||||||
|
private readonly Func<RuntimeGenerationToken> _currentGeneration;
|
||||||
|
private Binding? _current;
|
||||||
|
private long _nextBindingId;
|
||||||
|
|
||||||
|
internal RuntimePlacementProjectionRetrySlot(
|
||||||
|
Func<RuntimeGenerationToken> currentGeneration)
|
||||||
|
{
|
||||||
|
_currentGeneration = currentGeneration
|
||||||
|
?? throw new ArgumentNullException(nameof(currentGeneration));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal int BindingCount => _current is null ? 0 : 1;
|
||||||
|
|
||||||
|
internal IDisposable BindOwned(
|
||||||
|
RuntimeGenerationToken generation,
|
||||||
|
Func<bool> retry)
|
||||||
|
{
|
||||||
|
if (generation.Value == 0UL)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"A placement retry route requires a live Runtime generation.",
|
||||||
|
nameof(generation));
|
||||||
|
}
|
||||||
|
ArgumentNullException.ThrowIfNull(retry);
|
||||||
|
if (_current is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"A graphical placement retry route is already bound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var binding = new Binding(
|
||||||
|
checked(++_nextBindingId),
|
||||||
|
generation,
|
||||||
|
retry);
|
||||||
|
_current = binding;
|
||||||
|
return new DelegateDisposable(() => Unbind(binding));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RetryPending()
|
||||||
|
{
|
||||||
|
Binding? binding = _current;
|
||||||
|
if (binding is null
|
||||||
|
|| binding.Generation != _currentGeneration())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = binding.Retry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unbind(Binding binding)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_current, binding))
|
||||||
|
_current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DelegateDisposable(Action dispose) : IDisposable
|
||||||
|
{
|
||||||
|
private Action? _dispose = dispose
|
||||||
|
?? throw new ArgumentNullException(nameof(dispose));
|
||||||
|
|
||||||
|
public void Dispose() => Interlocked.Exchange(ref _dispose, null)?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,8 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
||||||
private readonly ILiveSpatialReconcilePhase _spatialReconciler;
|
private readonly ILiveSpatialReconcilePhase _spatialReconciler;
|
||||||
private readonly IWorldGenerationAvailability _availability;
|
private readonly IWorldGenerationAvailability _availability;
|
||||||
private readonly IRenderProjectionSyncPhase? _renderProjectionSync;
|
private readonly IRenderProjectionSyncPhase? _renderProjectionSync;
|
||||||
|
private readonly IRuntimePlacementProjectionRetryPhase?
|
||||||
|
_placementProjectionRetry;
|
||||||
|
|
||||||
public RetailLiveFrameCoordinator(
|
public RetailLiveFrameCoordinator(
|
||||||
ILiveObjectFramePhase objects,
|
ILiveObjectFramePhase objects,
|
||||||
|
|
@ -35,7 +37,8 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
||||||
IPostNetworkCommandFramePhase localPlayer,
|
IPostNetworkCommandFramePhase localPlayer,
|
||||||
ILiveSpatialReconcilePhase spatialReconciler,
|
ILiveSpatialReconcilePhase spatialReconciler,
|
||||||
IWorldGenerationAvailability? availability = null,
|
IWorldGenerationAvailability? availability = null,
|
||||||
IRenderProjectionSyncPhase? renderProjectionSync = null)
|
IRenderProjectionSyncPhase? renderProjectionSync = null,
|
||||||
|
IRuntimePlacementProjectionRetryPhase? placementProjectionRetry = null)
|
||||||
{
|
{
|
||||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||||
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
||||||
|
|
@ -45,6 +48,7 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
||||||
?? throw new ArgumentNullException(nameof(spatialReconciler));
|
?? throw new ArgumentNullException(nameof(spatialReconciler));
|
||||||
_availability = availability ?? AlwaysAvailableWorldGeneration.Instance;
|
_availability = availability ?? AlwaysAvailableWorldGeneration.Instance;
|
||||||
_renderProjectionSync = renderProjectionSync;
|
_renderProjectionSync = renderProjectionSync;
|
||||||
|
_placementProjectionRetry = placementProjectionRetry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Tick(float deltaSeconds)
|
public void Tick(float deltaSeconds)
|
||||||
|
|
@ -53,7 +57,14 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
||||||
if (_availability.IsWorldAvailable)
|
if (_availability.IsWorldAvailable)
|
||||||
_objects.Tick(frameDelta);
|
_objects.Tick(frameDelta);
|
||||||
using (_worldState.BeginMutationBatch())
|
using (_worldState.BeginMutationBatch())
|
||||||
|
{
|
||||||
_session.Tick();
|
_session.Tick();
|
||||||
|
// Streaming publication precedes this coordinator, while inbound
|
||||||
|
// network dispatch completes immediately above. A graphical
|
||||||
|
// receipt that previously lacked its destination backend can now
|
||||||
|
// retry against both readiness edges on the update thread.
|
||||||
|
_placementProjectionRetry?.RetryPending();
|
||||||
|
}
|
||||||
_localPlayer.RunPostNetworkCommandPhase();
|
_localPlayer.RunPostNetworkCommandPhase();
|
||||||
if (_availability.IsWorldAvailable)
|
if (_availability.IsWorldAvailable)
|
||||||
_spatialReconciler.Reconcile();
|
_spatialReconciler.Reconcile();
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ namespace AcDream.App.World;
|
||||||
/// and renderer/VFX visibility projections. Runtime physics, shadows, body
|
/// and renderer/VFX visibility projections. Runtime physics, shadows, body
|
||||||
/// state, clocks, and worksets were committed before this sink is invoked.
|
/// state, clocks, and worksets were committed before this sink is invoked.
|
||||||
///
|
///
|
||||||
/// This adapter deliberately owns no subscription: production composition
|
/// This adapter deliberately owns no subscription. The exact graphical
|
||||||
/// activates the shared observer only after both graphical and no-window hosts
|
/// session route owns the shared Runtime observer and its generation-scoped
|
||||||
/// implement the same presentation-only contract.
|
/// update-thread retry lease.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class RuntimePlacementPresentationSink
|
internal sealed class RuntimePlacementPresentationSink
|
||||||
: IRuntimePlacementProjectionSink
|
: IRuntimePlacementProjectionSink
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,25 @@ public sealed class RuntimePlacementProjectionSubscription
|
||||||
public RuntimePlacementProjectionSubscription(
|
public RuntimePlacementProjectionSubscription(
|
||||||
GameRuntime runtime,
|
GameRuntime runtime,
|
||||||
IRuntimePlacementProjectionSink sink)
|
IRuntimePlacementProjectionSink sink)
|
||||||
|
: this(runtime, sink, retryPendingOnSubscribe: true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribes before optionally draining the pending FIFO. A session route
|
||||||
|
/// which must publish its own disposal/retry ownership first passes
|
||||||
|
/// <c>false</c>, stores those owners, then calls <see cref="RetryPending"/>.
|
||||||
|
/// </summary>
|
||||||
|
public RuntimePlacementProjectionSubscription(
|
||||||
|
GameRuntime runtime,
|
||||||
|
IRuntimePlacementProjectionSink sink,
|
||||||
|
bool retryPendingOnSubscribe)
|
||||||
: this(
|
: this(
|
||||||
runtime?.Placements
|
runtime?.Placements
|
||||||
?? throw new ArgumentNullException(nameof(runtime)),
|
?? throw new ArgumentNullException(nameof(runtime)),
|
||||||
() => runtime.Generation,
|
() => runtime.Generation,
|
||||||
sink)
|
sink,
|
||||||
|
retryPendingOnSubscribe)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,13 +60,27 @@ public sealed class RuntimePlacementProjectionSubscription
|
||||||
RuntimePlacementProjectionChannel channel,
|
RuntimePlacementProjectionChannel channel,
|
||||||
Func<RuntimeGenerationToken> generation,
|
Func<RuntimeGenerationToken> generation,
|
||||||
IRuntimePlacementProjectionSink sink)
|
IRuntimePlacementProjectionSink sink)
|
||||||
|
: this(
|
||||||
|
channel,
|
||||||
|
generation,
|
||||||
|
sink,
|
||||||
|
retryPendingOnSubscribe: true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RuntimePlacementProjectionSubscription(
|
||||||
|
RuntimePlacementProjectionChannel channel,
|
||||||
|
Func<RuntimeGenerationToken> generation,
|
||||||
|
IRuntimePlacementProjectionSink sink,
|
||||||
|
bool retryPendingOnSubscribe)
|
||||||
{
|
{
|
||||||
_channel = channel ?? throw new ArgumentNullException(nameof(channel));
|
_channel = channel ?? throw new ArgumentNullException(nameof(channel));
|
||||||
_generation = generation
|
_generation = generation
|
||||||
?? throw new ArgumentNullException(nameof(generation));
|
?? throw new ArgumentNullException(nameof(generation));
|
||||||
_sink = sink ?? throw new ArgumentNullException(nameof(sink));
|
_sink = sink ?? throw new ArgumentNullException(nameof(sink));
|
||||||
_subscription = _channel.Subscribe(this);
|
_subscription = _channel.Subscribe(this);
|
||||||
_ = RetryPending();
|
if (retryPendingOnSubscribe)
|
||||||
|
_ = RetryPending();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasAppliedReceiptAwaitingAcknowledgement =>
|
public bool HasAppliedReceiptAwaitingAcknowledgement =>
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"LiveSessionRuntimeFactory.cs"));
|
"LiveSessionRuntimeFactory.cs"));
|
||||||
string sessionFactory = Slice(
|
string sessionFactory = Slice(
|
||||||
sessionFactorySource,
|
sessionFactorySource,
|
||||||
"private LiveSessionEventRouter CreateEventRouter(",
|
"private ILiveSessionEventRouting CreateEventRouter(",
|
||||||
"private LiveInventorySessionBindings CreateInventoryBindings()");
|
"private LiveInventorySessionBindings CreateInventoryBindings()");
|
||||||
string worldPhase = File.ReadAllText(Path.Combine(
|
string worldPhase = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
|
|
|
||||||
|
|
@ -5,28 +5,36 @@ namespace AcDream.App.Tests.Runtime;
|
||||||
public sealed class RuntimePhysicsOwnershipTests
|
public sealed class RuntimePhysicsOwnershipTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GraphicalPlacementProjectionRemainsDormantUntilCoordinatedCutover()
|
public void ProductionHostsUseSharedPlacementSubscriptionWithoutDirectChannel()
|
||||||
{
|
{
|
||||||
string root = FindRepositoryRoot();
|
string root = FindRepositoryRoot();
|
||||||
string relative = Path.Combine("src", "AcDream.App");
|
foreach (string relative in new[]
|
||||||
foreach (string file in Directory.EnumerateFiles(
|
{
|
||||||
Path.Combine(root, relative),
|
Path.Combine("src", "AcDream.App"),
|
||||||
"*.cs",
|
Path.Combine("src", "AcDream.Headless"),
|
||||||
SearchOption.AllDirectories))
|
})
|
||||||
{
|
{
|
||||||
string source = File.ReadAllText(file);
|
string[] sources = Directory.EnumerateFiles(
|
||||||
|
Path.Combine(root, relative),
|
||||||
|
"*.cs",
|
||||||
|
SearchOption.AllDirectories)
|
||||||
|
.Select(File.ReadAllText)
|
||||||
|
.ToArray();
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
".Placements.",
|
sources,
|
||||||
source,
|
source => source.Contains(
|
||||||
StringComparison.Ordinal);
|
".Placements.",
|
||||||
|
StringComparison.Ordinal));
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
"RuntimePlacementProjectionChannel",
|
sources,
|
||||||
source,
|
source => source.Contains(
|
||||||
StringComparison.Ordinal);
|
"RuntimePlacementProjectionChannel",
|
||||||
Assert.DoesNotContain(
|
StringComparison.Ordinal));
|
||||||
"RuntimePlacementProjectionSubscription",
|
Assert.Contains(
|
||||||
source,
|
sources,
|
||||||
StringComparison.Ordinal);
|
source => source.Contains(
|
||||||
|
"RuntimePlacementProjectionSubscription",
|
||||||
|
StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
using AcDream.App.Net;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
|
|
@ -11,6 +12,7 @@ using AcDream.Core.World;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
using AcDream.Runtime.Physics;
|
using AcDream.Runtime.Physics;
|
||||||
|
using AcDream.Runtime.Session;
|
||||||
using AcDream.Runtime.World;
|
using AcDream.Runtime.World;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
|
|
@ -409,6 +411,108 @@ public sealed class RuntimePlacementPresentationSinkTests
|
||||||
Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
|
Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GraphicalRoute_ReentrantInitialDrainTeardownLeavesHeadForReplacement()
|
||||||
|
{
|
||||||
|
using var fixture = SubscriptionFixture.Create();
|
||||||
|
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
|
||||||
|
fixture.Record.Canonical,
|
||||||
|
publishWithdrawal: true));
|
||||||
|
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
|
||||||
|
|
||||||
|
RuntimeGenerationToken generation = fixture.Generation;
|
||||||
|
var retries = new RuntimePlacementProjectionRetrySlot(
|
||||||
|
() => generation);
|
||||||
|
var firstEvents = new RecordingEventRoute();
|
||||||
|
GraphicalSessionEventRoute? first = null;
|
||||||
|
int applications = 0;
|
||||||
|
var sink = new DelegatePlacementSink(
|
||||||
|
(in RuntimePlacementProjectionSnapshot projection) =>
|
||||||
|
{
|
||||||
|
applications++;
|
||||||
|
bool applied = fixture.Sink.TryApply(in projection);
|
||||||
|
if (applications == 1)
|
||||||
|
first!.Dispose();
|
||||||
|
return applied;
|
||||||
|
});
|
||||||
|
first = new GraphicalSessionEventRoute(
|
||||||
|
firstEvents,
|
||||||
|
() => new RuntimePlacementProjectionSubscription(
|
||||||
|
fixture.Lifetime.Placements,
|
||||||
|
() => generation,
|
||||||
|
sink,
|
||||||
|
retryPendingOnSubscribe: false),
|
||||||
|
() => generation,
|
||||||
|
retries);
|
||||||
|
|
||||||
|
first.Attach();
|
||||||
|
|
||||||
|
Assert.Equal(1, applications);
|
||||||
|
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
|
||||||
|
Assert.Equal(0, retries.BindingCount);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.Events.PlacementSubscriberCount);
|
||||||
|
Assert.Equal(1, firstEvents.DisposeCount);
|
||||||
|
|
||||||
|
var replacementEvents = new RecordingEventRoute();
|
||||||
|
var replacement = new GraphicalSessionEventRoute(
|
||||||
|
replacementEvents,
|
||||||
|
() => new RuntimePlacementProjectionSubscription(
|
||||||
|
fixture.Lifetime.Placements,
|
||||||
|
() => generation,
|
||||||
|
sink,
|
||||||
|
retryPendingOnSubscribe: false),
|
||||||
|
() => generation,
|
||||||
|
retries);
|
||||||
|
replacement.Attach();
|
||||||
|
|
||||||
|
Assert.Equal(2, applications);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
|
||||||
|
Assert.Equal(1, retries.BindingCount);
|
||||||
|
Assert.Equal(1, fixture.Lifetime.Events.PlacementSubscriberCount);
|
||||||
|
|
||||||
|
replacement.Dispose();
|
||||||
|
|
||||||
|
Assert.Equal(0, retries.BindingCount);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.Events.PlacementSubscriberCount);
|
||||||
|
Assert.Equal(1, replacementEvents.DisposeCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RetrySlot_InvokesOnlyExactCurrentGenerationBinding()
|
||||||
|
{
|
||||||
|
RuntimeGenerationToken generation = new(7UL);
|
||||||
|
var retries = new RuntimePlacementProjectionRetrySlot(
|
||||||
|
() => generation);
|
||||||
|
int firstCalls = 0;
|
||||||
|
IDisposable first = retries.BindOwned(
|
||||||
|
generation,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
firstCalls++;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
retries.RetryPending();
|
||||||
|
generation = new RuntimeGenerationToken(8UL);
|
||||||
|
retries.RetryPending();
|
||||||
|
first.Dispose();
|
||||||
|
|
||||||
|
int replacementCalls = 0;
|
||||||
|
using IDisposable replacement = retries.BindOwned(
|
||||||
|
generation,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
replacementCalls++;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
first.Dispose();
|
||||||
|
retries.RetryPending();
|
||||||
|
|
||||||
|
Assert.Equal(1, firstCalls);
|
||||||
|
Assert.Equal(1, replacementCalls);
|
||||||
|
Assert.Equal(1, retries.BindingCount);
|
||||||
|
}
|
||||||
|
|
||||||
private static RuntimePlacementProjectionSnapshot Placement(
|
private static RuntimePlacementProjectionSnapshot Placement(
|
||||||
Fixture fixture,
|
Fixture fixture,
|
||||||
LiveEntityRecord record,
|
LiveEntityRecord record,
|
||||||
|
|
@ -805,6 +909,27 @@ public sealed class RuntimePlacementPresentationSinkTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class DelegatePlacementSink(
|
||||||
|
PlacementApply apply) : IRuntimePlacementProjectionSink
|
||||||
|
{
|
||||||
|
public bool TryApply(
|
||||||
|
in RuntimePlacementProjectionSnapshot projection) =>
|
||||||
|
apply(in projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private delegate bool PlacementApply(
|
||||||
|
in RuntimePlacementProjectionSnapshot projection);
|
||||||
|
|
||||||
|
private sealed class RecordingEventRoute : ILiveSessionEventRouting
|
||||||
|
{
|
||||||
|
public int AttachCount { get; private set; }
|
||||||
|
public int DisposeCount { get; private set; }
|
||||||
|
|
||||||
|
public void Attach() => AttachCount++;
|
||||||
|
|
||||||
|
public void Dispose() => DisposeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class RecordingResources : ILiveEntityResourceLifecycle
|
private sealed class RecordingResources : ILiveEntityResourceLifecycle
|
||||||
{
|
{
|
||||||
public void Register(WorldEntity entity) { }
|
public void Register(WorldEntity entity) { }
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Net;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.Update;
|
using AcDream.App.Update;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
|
using AcDream.Runtime.Session;
|
||||||
using AcDream.Runtime.World;
|
using AcDream.Runtime.World;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.World;
|
namespace AcDream.App.Tests.World;
|
||||||
|
|
@ -387,6 +389,65 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
Assert.Equal(1, CountOccurrences(source, "_scripts.Tick("));
|
Assert.Equal(1, CountOccurrences(source, "_scripts.Tick("));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlacementRetryRunsAfterStreamingAndInboundBeforeCommandReconcile()
|
||||||
|
{
|
||||||
|
string root = FindRepoRoot();
|
||||||
|
string outer = File.ReadAllText(Path.Combine(
|
||||||
|
root,
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Update",
|
||||||
|
"UpdateFrameOrchestrator.cs"));
|
||||||
|
string live = File.ReadAllText(Path.Combine(
|
||||||
|
root,
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"World",
|
||||||
|
"RetailLiveFrameCoordinator.cs"));
|
||||||
|
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
outer,
|
||||||
|
"_streaming.Tick();",
|
||||||
|
"_liveFrame.Tick(");
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
live,
|
||||||
|
"_session.Tick();",
|
||||||
|
"_placementProjectionRetry?.RetryPending();",
|
||||||
|
"_localPlayer.RunPostNetworkCommandPhase();",
|
||||||
|
"_spatialReconciler.Reconcile();");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void LiveFrameRetriesPlacementAfterInboundEvenWhenWorldIsQuiesced(
|
||||||
|
bool worldAvailable)
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var phases = new RecordingLivePhases(calls)
|
||||||
|
{
|
||||||
|
IsWorldAvailable = worldAvailable,
|
||||||
|
};
|
||||||
|
var frame = new RetailLiveFrameCoordinator(
|
||||||
|
phases,
|
||||||
|
new GpuWorldState(),
|
||||||
|
phases,
|
||||||
|
phases,
|
||||||
|
phases,
|
||||||
|
phases,
|
||||||
|
phases,
|
||||||
|
phases);
|
||||||
|
|
||||||
|
frame.Tick(1f / 60f);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
worldAvailable
|
||||||
|
? ["objects", "network", "placement-retry", "commands", "reconcile"]
|
||||||
|
: ["network", "placement-retry", "commands", "render-sync"],
|
||||||
|
calls);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GameWindow_ComposesTheLiveFrameOwnersWithoutOwningTheirBodies()
|
public void GameWindow_ComposesTheLiveFrameOwnersWithoutOwningTheirBodies()
|
||||||
{
|
{
|
||||||
|
|
@ -1048,6 +1109,26 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingLivePhases(List<string> calls) :
|
||||||
|
ILiveObjectFramePhase,
|
||||||
|
IRuntimeLiveSessionFramePhase,
|
||||||
|
IPostNetworkCommandFramePhase,
|
||||||
|
ILiveSpatialReconcilePhase,
|
||||||
|
IRenderProjectionSyncPhase,
|
||||||
|
IWorldGenerationAvailability,
|
||||||
|
IRuntimePlacementProjectionRetryPhase
|
||||||
|
{
|
||||||
|
public bool IsWorldAvailable { get; set; }
|
||||||
|
public long QuiescedGeneration => IsWorldAvailable ? 0L : 1L;
|
||||||
|
|
||||||
|
public void Tick(float deltaSeconds) => calls.Add("objects");
|
||||||
|
public void Tick() => calls.Add("network");
|
||||||
|
public void RunPostNetworkCommandPhase() => calls.Add("commands");
|
||||||
|
public void Reconcile() => calls.Add("reconcile");
|
||||||
|
public void SynchronizeActiveSources() => calls.Add("render-sync");
|
||||||
|
public void RetryPending() => calls.Add("placement-retry");
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class RecordingCommit(List<string> calls)
|
private sealed class RecordingCommit(List<string> calls)
|
||||||
: IUpdateFrameCommitPhase
|
: IUpdateFrameCommitPhase
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,28 @@ public sealed class RuntimePlacementProjectionSubscriptionTests
|
||||||
Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
|
Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SubscribeWithoutDrainPublishesOnlyAfterExplicitRetry()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture();
|
||||||
|
RuntimeSetPositionOutcome pending = fixture.Place(
|
||||||
|
fixture.First,
|
||||||
|
new Vector3(18.5f, 18f, 7f));
|
||||||
|
var sink = new RecordingSink();
|
||||||
|
|
||||||
|
using var subscription = fixture.Subscribe(
|
||||||
|
sink,
|
||||||
|
retryPendingOnSubscribe: false);
|
||||||
|
|
||||||
|
Assert.Empty(sink.Applied);
|
||||||
|
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
|
||||||
|
|
||||||
|
Assert.True(subscription.RetryPending());
|
||||||
|
|
||||||
|
Assert.Equal(pending.Projection, Assert.Single(sink.Applied).Token);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SyntheticWithdrawalUsesTheSameExactSinkAndAckPath()
|
public void SyntheticWithdrawalUsesTheSameExactSinkAndAckPath()
|
||||||
{
|
{
|
||||||
|
|
@ -263,10 +285,12 @@ public sealed class RuntimePlacementProjectionSubscriptionTests
|
||||||
internal RuntimeEntityRecord? Second { get; }
|
internal RuntimeEntityRecord? Second { get; }
|
||||||
|
|
||||||
internal RuntimePlacementProjectionSubscription Subscribe(
|
internal RuntimePlacementProjectionSubscription Subscribe(
|
||||||
IRuntimePlacementProjectionSink sink) => new(
|
IRuntimePlacementProjectionSink sink,
|
||||||
|
bool retryPendingOnSubscribe = true) => new(
|
||||||
Lifetime.Placements,
|
Lifetime.Placements,
|
||||||
() => _generation,
|
() => _generation,
|
||||||
sink);
|
sink,
|
||||||
|
retryPendingOnSubscribe);
|
||||||
|
|
||||||
internal RuntimeSetPositionOutcome Place(
|
internal RuntimeSetPositionOutcome Place(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue