diff --git a/src/AcDream.Runtime/Physics/RuntimePlacementProjectionSubscription.cs b/src/AcDream.Runtime/Physics/RuntimePlacementProjectionSubscription.cs
new file mode 100644
index 00000000..bc48214c
--- /dev/null
+++ b/src/AcDream.Runtime/Physics/RuntimePlacementProjectionSubscription.cs
@@ -0,0 +1,119 @@
+using AcDream.Runtime.Entities;
+
+namespace AcDream.Runtime.Physics;
+
+///
+/// Presentation-only sink for canonical Runtime SetPosition receipts.
+/// Implementations must apply an exact receipt idempotently: acknowledgement
+/// can fail after a successful projection when a re-entrant Runtime mutation
+/// revises the FIFO head, and the same immutable receipt may then be retried.
+///
+public interface IRuntimePlacementProjectionSink
+{
+ bool TryApply(in RuntimePlacementProjectionSnapshot projection);
+}
+
+///
+/// Shared graphical/no-window subscription which projects only the oldest
+/// canonical receipt and acknowledges it only after the host sink succeeds.
+/// Runtime remains the sole position, collision, residence, and lifetime
+/// authority; this class owns only its observer subscription and one
+/// idempotency token for a projection that succeeded before acknowledgement.
+///
+public sealed class RuntimePlacementProjectionSubscription
+ : IRuntimePlacementObserver,
+ IDisposable
+{
+ private readonly RuntimePlacementProjectionChannel _channel;
+ private readonly Func _generation;
+ private readonly IRuntimePlacementProjectionSink _sink;
+ private IDisposable? _subscription;
+ private RuntimePlacementProjectionToken _appliedAwaitingAcknowledgement;
+ private bool _disposed;
+
+ public RuntimePlacementProjectionSubscription(
+ GameRuntime runtime,
+ IRuntimePlacementProjectionSink sink)
+ : this(
+ runtime?.Placements
+ ?? throw new ArgumentNullException(nameof(runtime)),
+ () => runtime.Generation,
+ sink)
+ {
+ }
+
+ internal RuntimePlacementProjectionSubscription(
+ RuntimePlacementProjectionChannel channel,
+ Func generation,
+ IRuntimePlacementProjectionSink sink)
+ {
+ _channel = channel ?? throw new ArgumentNullException(nameof(channel));
+ _generation = generation
+ ?? throw new ArgumentNullException(nameof(generation));
+ _sink = sink ?? throw new ArgumentNullException(nameof(sink));
+ _subscription = _channel.Subscribe(this);
+ _ = RetryPending();
+ }
+
+ public bool HasAppliedReceiptAwaitingAcknowledgement =>
+ _appliedAwaitingAcknowledgement.IsValid;
+
+ ///
+ /// Republishes Runtime's complete still-pending FIFO. Later receipts are
+ /// ignored until the exact oldest receipt projects and acknowledges.
+ ///
+ public bool RetryPending()
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ RuntimeGenerationToken generation = _generation();
+ if (_appliedAwaitingAcknowledgement.IsValid
+ && (!_channel.TryPeek(
+ generation,
+ out RuntimePlacementProjectionSnapshot head)
+ || head.Token != _appliedAwaitingAcknowledgement))
+ {
+ _appliedAwaitingAcknowledgement = default;
+ }
+ return _channel.RetryPending(generation);
+ }
+
+ public void OnPlacement(in RuntimePlacementDelta delta)
+ {
+ if (_disposed
+ || !_channel.TryPeek(
+ delta.Stamp.Generation,
+ out RuntimePlacementProjectionSnapshot head)
+ || head != delta.Placement)
+ {
+ return;
+ }
+
+ RuntimePlacementProjectionToken token = head.Token;
+ if (_appliedAwaitingAcknowledgement != token)
+ {
+ if (!_sink.TryApply(in head))
+ return;
+ // A sink can synchronously tear down its host while applying a
+ // receipt. Leave that receipt pending for the replacement host;
+ // disposal is never permission to acknowledge afterward.
+ if (_disposed)
+ return;
+ _appliedAwaitingAcknowledgement = token;
+ }
+
+ if (_channel.Acknowledge(delta.Stamp.Generation, token)
+ && _appliedAwaitingAcknowledgement == token)
+ {
+ _appliedAwaitingAcknowledgement = default;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+ _disposed = true;
+ Interlocked.Exchange(ref _subscription, null)?.Dispose();
+ _appliedAwaitingAcknowledgement = default;
+ }
+}
diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimePlacementProjectionSubscriptionTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimePlacementProjectionSubscriptionTests.cs
new file mode 100644
index 00000000..541a0ac3
--- /dev/null
+++ b/tests/AcDream.Runtime.Tests/Physics/RuntimePlacementProjectionSubscriptionTests.cs
@@ -0,0 +1,415 @@
+using System.Collections.Immutable;
+using System.Numerics;
+using AcDream.Core.Net;
+using AcDream.Core.Net.Messages;
+using AcDream.Core.Physics;
+using AcDream.Runtime.Entities;
+using AcDream.Runtime.Physics;
+
+namespace AcDream.Runtime.Tests.Physics;
+
+public sealed class RuntimePlacementProjectionSubscriptionTests
+{
+ private const uint Landblock = 0xA9B40000u;
+ private const uint Cell = Landblock | 0x0001u;
+
+ [Fact]
+ public void SuccessfulSinkAcknowledgesExactPlaceSynchronously()
+ {
+ using var fixture = new Fixture();
+ var sink = new RecordingSink();
+ using var subscription = fixture.Subscribe(sink);
+
+ RuntimeSetPositionOutcome outcome = fixture.Place(
+ fixture.First,
+ new Vector3(12f, 18f, 7f));
+
+ Assert.Equal(
+ RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
+ outcome.Status);
+ RuntimePlacementProjectionSnapshot only = Assert.Single(sink.Applied);
+ Assert.Equal(RuntimePlacementProjectionKind.Place, only.Kind);
+ Assert.Equal(outcome.Projection, only.Token);
+ Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
+ Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
+ }
+
+ [Fact]
+ public void RejectedSinkRetainsHeadAndRetryAppliesItExactly()
+ {
+ using var fixture = new Fixture();
+ bool ready = false;
+ var sink = new RecordingSink(_ => ready);
+ using var subscription = fixture.Subscribe(sink);
+ RuntimeSetPositionOutcome outcome = fixture.Place(
+ fixture.First,
+ new Vector3(13f, 18f, 7f));
+
+ Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
+ Assert.Single(sink.Applied);
+ Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
+
+ ready = true;
+ Assert.True(subscription.RetryPending());
+
+ Assert.Equal(2, sink.Applied.Count);
+ Assert.All(sink.Applied,
+ projection => Assert.Equal(outcome.Projection, projection.Token));
+ Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
+ }
+
+ [Fact]
+ public void ThrowingSinkLeavesReceiptRetryableAndRecordsDispatchFailure()
+ {
+ using var fixture = new Fixture();
+ bool fail = true;
+ var sink = new RecordingSink(_ =>
+ {
+ if (fail)
+ throw new InvalidOperationException("host unavailable");
+ return true;
+ });
+ using var subscription = fixture.Subscribe(sink);
+ _ = fixture.Place(fixture.First, new Vector3(14f, 18f, 7f));
+
+ Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
+ Assert.Equal(1, fixture.Lifetime.Events.DispatchFailureCount);
+ Assert.IsType(
+ fixture.Lifetime.Events.LastDispatchFailure);
+
+ fail = false;
+ Assert.True(subscription.RetryPending());
+
+ Assert.Equal(2, sink.Applied.Count);
+ Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
+ }
+
+ [Fact]
+ public void LaterReceiptCannotProjectBeforeRejectedFifoHead()
+ {
+ using var fixture = new Fixture(secondEntity: true);
+ bool ready = false;
+ RuntimeEntityKey firstKey = fixture.First.Key!.Value;
+ var sink = new RecordingSink(projection =>
+ ready || projection.Token.Entity != firstKey);
+ using var subscription = fixture.Subscribe(sink);
+
+ _ = fixture.Place(fixture.First, new Vector3(15f, 18f, 7f));
+ _ = fixture.Place(fixture.Second!, new Vector3(16f, 18f, 7f));
+
+ RuntimePlacementProjectionSnapshot initial = Assert.Single(sink.Applied);
+ Assert.Equal(firstKey, initial.Token.Entity);
+ Assert.Equal(2, fixture.Lifetime.Placements.PendingCount);
+
+ ready = true;
+ Assert.True(subscription.RetryPending());
+
+ Assert.Equal(
+ [firstKey, firstKey, fixture.Second!.Key!.Value],
+ sink.Applied.Select(item => item.Token.Entity).ToArray());
+ Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
+ }
+
+ [Fact]
+ public void ReentrantReplacementProjectsRevisedDiscardBeforeAcknowledging()
+ {
+ using var fixture = new Fixture();
+ RuntimeEntityPlacementToken replacement = default;
+ var sink = new RecordingSink(projection =>
+ {
+ if (projection.Kind is RuntimePlacementProjectionKind.Place)
+ {
+ replacement = fixture.Lifetime.Physics.SetPosition
+ .BeginAcceptedPlacement(
+ fixture.First,
+ fixture.First.PositionAuthorityVersion,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative);
+ }
+ return true;
+ });
+ using var subscription = fixture.Subscribe(sink);
+
+ RuntimeSetPositionOutcome original = fixture.Place(
+ fixture.First,
+ new Vector3(17f, 18f, 7f));
+
+ Assert.True(replacement.IsValid);
+ Assert.Equal(2, sink.Applied.Count);
+ Assert.Equal(RuntimePlacementProjectionKind.Place,
+ sink.Applied[0].Kind);
+ Assert.Equal(RuntimePlacementProjectionKind.Discard,
+ sink.Applied[1].Kind);
+ Assert.Equal(original.Projection.Sequence,
+ sink.Applied[1].Token.Sequence);
+ Assert.True(sink.Applied[1].Token.Revision
+ > original.Projection.Revision);
+ Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
+ Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
+
+ fixture.Lifetime.Physics.SetPosition.Forget(
+ fixture.First,
+ releasePreparedMover: true);
+ }
+
+ [Fact]
+ public void ConstructorDrainsReceiptsCreatedBeforeSubscriptionInFifoOrder()
+ {
+ using var fixture = new Fixture(secondEntity: true);
+ RuntimeSetPositionOutcome first = fixture.Place(
+ fixture.First,
+ new Vector3(18f, 18f, 7f));
+ RuntimeSetPositionOutcome second = fixture.Place(
+ fixture.Second!,
+ new Vector3(19f, 18f, 7f));
+ Assert.Equal(2, fixture.Lifetime.Placements.PendingCount);
+ var sink = new RecordingSink();
+
+ using var subscription = fixture.Subscribe(sink);
+
+ Assert.Equal(
+ [first.Projection, second.Projection],
+ sink.Applied.Select(item => item.Token).ToArray());
+ Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
+ Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
+ }
+
+ [Fact]
+ public void SyntheticWithdrawalUsesTheSameExactSinkAndAckPath()
+ {
+ using var fixture = new Fixture();
+ var sink = new RecordingSink();
+ using var subscription = fixture.Subscribe(sink);
+
+ Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
+ fixture.First,
+ publishWithdrawal: true));
+
+ RuntimePlacementProjectionSnapshot withdrawal = Assert.Single(
+ sink.Applied);
+ Assert.Equal(RuntimePlacementProjectionKind.Withdraw, withdrawal.Kind);
+ Assert.Equal(fixture.First.Key, withdrawal.Token.Entity);
+ Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
+ Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
+ }
+
+ [Fact]
+ public void DisposeUnsubscribesWithoutConsumingPendingRuntimeReceipt()
+ {
+ using var fixture = new Fixture();
+ var sink = new RecordingSink(_ => false);
+ var subscription = fixture.Subscribe(sink);
+ _ = fixture.Place(fixture.First, new Vector3(18f, 18f, 7f));
+ Assert.Equal(1, fixture.Lifetime.Events.PlacementSubscriberCount);
+
+ subscription.Dispose();
+
+ Assert.Equal(0, fixture.Lifetime.Events.PlacementSubscriberCount);
+ Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
+ Assert.Throws(() =>
+ subscription.RetryPending());
+
+ var replacementSink = new RecordingSink();
+ using var replacement = fixture.Subscribe(replacementSink);
+ Assert.Single(replacementSink.Applied);
+ Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
+ }
+
+ [Fact]
+ public void ReentrantSinkDisposalNeverAcknowledgesAfterHostTeardown()
+ {
+ using var fixture = new Fixture();
+ RuntimePlacementProjectionSubscription? subscription = null;
+ var sink = new RecordingSink(_ =>
+ {
+ subscription!.Dispose();
+ return true;
+ });
+ subscription = fixture.Subscribe(sink);
+
+ _ = fixture.Place(fixture.First, new Vector3(19f, 18f, 7f));
+
+ Assert.Single(sink.Applied);
+ Assert.Equal(0, fixture.Lifetime.Events.PlacementSubscriberCount);
+ Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
+ Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
+
+ var replacementSink = new RecordingSink();
+ using var replacement = fixture.Subscribe(replacementSink);
+ Assert.Single(replacementSink.Applied);
+ Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
+ }
+
+ private sealed class Fixture : IDisposable
+ {
+ private readonly RuntimeGenerationToken _generation = new(7UL);
+
+ internal Fixture(bool secondEntity = false)
+ {
+ Lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ Lifetime.BindEventContext(
+ () => _generation,
+ static () => 11UL);
+ First = CreateRecord(Lifetime, 0x70002001u, 1);
+ AttachBody(Lifetime, First, new Vector3(10f, 20f, 7f));
+ if (secondEntity)
+ {
+ Second = CreateRecord(Lifetime, 0x70002002u, 1);
+ AttachBody(Lifetime, Second, new Vector3(11f, 20f, 7f));
+ }
+ }
+
+ internal RuntimeEntityObjectLifetime Lifetime { get; }
+ internal RuntimeEntityRecord First { get; }
+ internal RuntimeEntityRecord? Second { get; }
+
+ internal RuntimePlacementProjectionSubscription Subscribe(
+ IRuntimePlacementProjectionSink sink) => new(
+ Lifetime.Placements,
+ () => _generation,
+ sink);
+
+ internal RuntimeSetPositionOutcome Place(
+ RuntimeEntityRecord record,
+ Vector3 position) => Lifetime.Physics.SetPosition.Apply(
+ record,
+ record.PositionAuthorityVersion,
+ Command(position));
+
+ public void Dispose() => Lifetime.Dispose();
+ }
+
+ private sealed class RecordingSink(
+ Func? apply = null)
+ : IRuntimePlacementProjectionSink
+ {
+ internal List Applied { get; } = [];
+
+ public bool TryApply(
+ in RuntimePlacementProjectionSnapshot projection)
+ {
+ Applied.Add(projection);
+ return apply?.Invoke(projection) ?? true;
+ }
+ }
+
+ private static RuntimeSetPositionCommand Command(Vector3 position) => new(
+ new PhysicsSetPositionRequest(
+ position,
+ Quaternion.Identity,
+ Cell,
+ position,
+ ImmutableArray.Empty,
+ Scale: 1f,
+ StepUpHeight: 0.4f,
+ StepDownHeight: 0.4f,
+ Flags: PhysicsSetPositionFlags.Placement
+ | PhysicsSetPositionFlags.Slide),
+ RuntimeSetPositionOperationKind.RemoteAuthoritative,
+ GameTime: 10d,
+ ExpectedVelocityAuthorityVersion: 0UL);
+
+ private static RuntimeEntityRecord CreateRecord(
+ RuntimeEntityObjectLifetime lifetime,
+ uint guid,
+ ushort incarnation)
+ {
+ RuntimeEntityRecord record = lifetime.RegisterEntity(
+ Spawn(guid, incarnation)).Canonical!;
+ lifetime.Entities.SetFinalPhysicsState(
+ record,
+ PhysicsStateFlags.Gravity);
+ return record;
+ }
+
+ private static void AttachBody(
+ RuntimeEntityObjectLifetime lifetime,
+ RuntimeEntityRecord record,
+ Vector3 position)
+ {
+ lifetime.Entities.SetFullCell(record, Cell, Landblock | 0xFFFFu);
+ var body = new PhysicsBody
+ {
+ Position = position,
+ Orientation = Quaternion.Identity,
+ LastUpdateTime = 1d,
+ State = PhysicsStateFlags.Gravity,
+ TransientState = TransientStateFlags.Active,
+ };
+ body.SnapToCell(Cell, position, position);
+ lifetime.Entities.SetPhysicsBody(record, body);
+ record.ObjectClock.Activate();
+ lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
+ }
+
+ private static PhysicsEngine FlatEngine()
+ {
+ var engine = new PhysicsEngine
+ {
+ DataCache = new PhysicsDataCache(),
+ };
+ engine.AddLandblock(
+ Landblock,
+ new TerrainSurface(new byte[81], new float[256]),
+ Array.Empty(),
+ Array.Empty(),
+ worldOffsetX: 0f,
+ worldOffsetY: 0f);
+ return engine;
+ }
+
+ private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance)
+ {
+ var position = new CreateObject.ServerPosition(
+ Cell, 10f, 20f, 7f, 1f, 0f, 0f, 0f);
+ var timestamps = new PhysicsTimestamps(
+ Position: 1,
+ Movement: 1,
+ State: 1,
+ Vector: 1,
+ Teleport: 0,
+ ServerControlledMove: 1,
+ ForcePosition: 0,
+ ObjDesc: 1,
+ Instance: instance);
+ var physics = new PhysicsSpawnData(
+ RawState: (uint)PhysicsStateFlags.Gravity,
+ Position: position,
+ Movement: null,
+ AnimationFrame: null,
+ SetupTableId: 0x02000001u,
+ MotionTableId: 0x09000001u,
+ SoundTableId: null,
+ PhysicsScriptTableId: null,
+ Parent: null,
+ Children: null,
+ Scale: null,
+ Friction: null,
+ Elasticity: null,
+ Translucency: null,
+ Velocity: null,
+ Acceleration: null,
+ AngularVelocity: null,
+ DefaultScriptType: null,
+ DefaultScriptIntensity: null,
+ Timestamps: timestamps);
+ return new WorldSession.EntitySpawn(
+ guid,
+ position,
+ 0x02000001u,
+ Array.Empty(),
+ Array.Empty(),
+ Array.Empty(),
+ null,
+ null,
+ "placement-projection-fixture",
+ null,
+ null,
+ 0x09000001u,
+ PhysicsState: (uint)PhysicsStateFlags.Gravity,
+ InstanceSequence: instance,
+ MovementSequence: 1,
+ ServerControlSequence: 1,
+ PositionSequence: 1,
+ Physics: physics);
+ }
+}