feat(app): observe canonical placement receipts
This commit is contained in:
parent
378ca95a67
commit
f05ed5c3cd
14 changed files with 545 additions and 31 deletions
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,
|
||||
LiveEntityPresentationController Presentation,
|
||||
RemoteMovementObservationTracker RemoteMovementObservations,
|
||||
RenderSceneShadowRuntime? RenderSceneShadow);
|
||||
RenderSceneShadowRuntime? RenderSceneShadow,
|
||||
RuntimePlacementPresentationSink PlacementProjection,
|
||||
RuntimePlacementProjectionRetrySlot PlacementRetries);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the exact per-generation route/reset graph for the canonical live
|
||||
|
|
@ -225,7 +227,7 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
_player.WorldOrigin.Reset();
|
||||
}
|
||||
|
||||
private LiveSessionEventRouter CreateEventRouter(WorldSession session)
|
||||
private ILiveSessionEventRouting CreateEventRouter(WorldSession session)
|
||||
{
|
||||
SkillTable? skillTable = _world.Dats.Get<SkillTable>(0x0E000004u);
|
||||
if (_ui.CharacterSheet is not null)
|
||||
|
|
@ -235,7 +237,7 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
CharacterSheetProvider.LoadExperienceTable(_world.Dats, _log);
|
||||
}
|
||||
|
||||
return new LiveSessionEventRouter(
|
||||
var route = new LiveSessionEventRouter(
|
||||
session,
|
||||
_world.EntitySession.CreateSink(),
|
||||
new LiveEnvironmentSessionSink(
|
||||
|
|
@ -248,6 +250,11 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
_domain.Communication.TurbineChat,
|
||||
_domain.Communication.Friends,
|
||||
_domain.Communication.Squelch));
|
||||
return new GraphicalSessionEventRoute(
|
||||
route,
|
||||
_domain.Runtime,
|
||||
_world.PlacementProjection,
|
||||
_world.PlacementRetries);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue