feat(runtime): expose dormant placement receipts

This commit is contained in:
Erik 2026-07-31 23:11:44 +02:00
parent 4c02ac4259
commit 270f5154b9
9 changed files with 223 additions and 4 deletions

View file

@ -118,6 +118,9 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InventoryView = views.Inventory;
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
Physics.SetPosition.BindEventStream(Events);
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
}
internal RuntimeEntityObjectLifetime(
@ -140,6 +143,9 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InventoryView = views.Inventory;
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
Physics.SetPosition.BindEventStream(Events);
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
}
internal RuntimeEntityObjectLifetime(
@ -162,6 +168,9 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InventoryView = views.Inventory;
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
Physics.SetPosition.BindEventStream(Events);
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
}
public RuntimeEntityDirectory Entities { get; }
@ -170,6 +179,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
public IRuntimeEntityView EntityView { get; }
public IRuntimeInventoryView InventoryView { get; }
public RuntimeEntityObjectEventStream Events { get; }
public RuntimePlacementProjectionChannel Placements { get; }
public RuntimeEntityObjectOwnershipSnapshot CaptureOwnership()
{
@ -204,6 +214,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
{
EnsureNotDisposed();
Events.BindContext(generation, frameNumber);
Placements.BindGeneration(generation);
}
/// <summary>

View file

@ -1,5 +1,6 @@
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using AcDream.Runtime.World;
@ -303,6 +304,8 @@ public sealed class GameRuntime
public RuntimeWorldEnvironmentState EnvironmentOwner { get; }
public RuntimeWorldTransitState TransitOwner { get; }
public RuntimeGenerationReset GenerationReset { get; }
public RuntimePlacementProjectionChannel Placements =>
EntityObjects.Placements;
public RuntimeGenerationToken Generation => Session.Generation;

View file

@ -0,0 +1,88 @@
using AcDream.Runtime.Entities;
namespace AcDream.Runtime.Physics;
/// <summary>
/// Public host boundary for Runtime-owned SetPosition projection receipts.
/// The channel owns no placement state: observation, retry, and exact-token
/// acknowledgement delegate to the canonical entity lifetime's event stream
/// and SetPosition owner.
/// </summary>
public sealed class RuntimePlacementProjectionChannel
{
private readonly RuntimeEntityObjectEventStream _events;
private readonly RuntimeSetPositionState _setPosition;
private Func<RuntimeGenerationToken> _generation = static () => default;
private bool _generationBound;
internal RuntimePlacementProjectionChannel(
RuntimeEntityObjectEventStream events,
RuntimeSetPositionState setPosition)
{
_events = events ?? throw new ArgumentNullException(nameof(events));
_setPosition = setPosition
?? throw new ArgumentNullException(nameof(setPosition));
}
/// <summary>
/// Observes ordered immutable projection receipts on the Runtime commit
/// thread. A host must acknowledge only after its projection succeeds.
/// </summary>
public IDisposable Subscribe(IRuntimePlacementObserver observer) =>
_events.SubscribePlacement(observer);
internal void BindGeneration(Func<RuntimeGenerationToken> generation)
{
ArgumentNullException.ThrowIfNull(generation);
if (_generationBound)
{
throw new InvalidOperationException(
"The Runtime placement generation source is already bound.");
}
_generation = generation;
_generationBound = true;
}
/// <summary>
/// Acknowledges only the exact oldest outstanding receipt. Stale,
/// reordered, superseded, or already acknowledged tokens are rejected.
/// </summary>
public bool Acknowledge(
RuntimeGenerationToken expectedGeneration,
in RuntimePlacementProjectionToken token) =>
IsCurrent(expectedGeneration)
&& _setPosition.AcknowledgeProjection(token);
/// <summary>
/// Republishes every still-pending immutable receipt in canonical order.
/// Runtime authority is never replayed or recommitted by a retry.
/// </summary>
public bool RetryPending(RuntimeGenerationToken expectedGeneration)
{
if (!IsCurrent(expectedGeneration))
return false;
_setPosition.RetryPendingProjections();
return true;
}
/// <summary>
/// Returns the exact oldest outstanding receipt without consuming it.
/// </summary>
public bool TryPeek(
RuntimeGenerationToken expectedGeneration,
out RuntimePlacementProjectionSnapshot projection)
{
if (IsCurrent(expectedGeneration))
return _setPosition.TryPeekProjection(out projection);
projection = default;
return false;
}
public int PendingCount => _setPosition.PendingProjectionCount;
private bool IsCurrent(RuntimeGenerationToken expectedGeneration) =>
_generationBound
&& expectedGeneration.Value != 0UL
&& expectedGeneration == _generation();
}

View file

@ -283,6 +283,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
_preparedMovers.Count);
}
internal int PendingProjectionCount => _pendingProjection.Count;
internal void BindEventStream(RuntimeEntityObjectEventStream events)
{
EnsureNotDisposed();