feat(runtime): bridge executor completion to the placement stream
Cutover slice C0 (docs/plans/2026-08-02-placement-cutover.md): the seam work that lets C3 flip hosts onto a complete receipt stream instead of growing one mid-cutover. The executor's Released exit now publishes an acknowledge-only ExecutorCompleted receipt through the one placement projection stream — registered before observer dispatch, correlated to the full execution receipt, reaped exactly once on acknowledgement/ discard/session-clear, and counted in the convergence ledger. All three production placement sinks acknowledge-and-ignore the new kind via early returns proven behavior-preserving for every existing kind; without them the first such receipt at cutover would permanently wedge the exact-head FIFO behind sinks that return false. Provably inert today: the publisher has no production caller. Execute's live inputs now derive from Runtime's own owners bound at GameRuntime construction: UsePositionFromServer is retail's exact autonomy_level != 2 (CommandInterpreter::UsePositionFromServer 0x006B3B40, startup-only knob), and PlayerDistance uses the live movement controller's position with a null-safe fallback to the caller struct — never a fabricated origin. TryPrepareAndSubmitAuthoredPlacement chains the prepared-collision Setup read through PrepareMover to submission with zero validation-semantics changes. TryCommitParent and CommitWithdrawal gain the sibling cancellation flow (residence + ordinary placement family); TryCommitParent deliberately omits LeaveWorld — retail's set_parent performs its single gated leave_world (0x00515A90) and a second would have no counterpart. Not fully dormant: the two cancellation fixes change Runtime paths production already calls (today as no-op-adjacent hardening, since nothing upstream begins a residence yet); everything else is reachable only by tests. Reviewed: retail-conformance PASS + architecture/ adversarial PASS after one fix round (sink wedge, completion-receipt lifecycle, null-controller distance). Runtime 921/921; complete Release solution 10,716 passed / 4 intentional skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
27e05b99e4
commit
67f63e85e5
14 changed files with 1639 additions and 11 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
|
@ -59,6 +60,17 @@ public enum RuntimePlacementProjectionKind
|
|||
Withdraw,
|
||||
Place,
|
||||
Discard,
|
||||
/// <summary>
|
||||
/// C0-1: the initial-create continuation executor's own FIFO drain has
|
||||
/// finished for this entity (not itself a SetPosition operation - the
|
||||
/// residence lease is already released by the time this publishes).
|
||||
/// Published on the SAME ordered stream every Place/Withdraw/Discard
|
||||
/// receipt uses so a host learns "this entity's placement committed and
|
||||
/// its FIFO drained" through the one already-built observer seam,
|
||||
/// instead of a second stream/queue. Acknowledge-only, like Discard -
|
||||
/// see AcknowledgeProjection's dedicated branch.
|
||||
/// </summary>
|
||||
ExecutorCompleted,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimePortalPlacementAuthority(
|
||||
|
|
@ -432,6 +444,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
private ulong _nextCollisionPrefixQuiescenceOperationId;
|
||||
private ulong _nextLostDeadlineSequence;
|
||||
private RuntimeEntityObjectEventStream? _events;
|
||||
private Action<RuntimeEntityKey, ulong>? _executorCompletionAcknowledged;
|
||||
private bool _disposed;
|
||||
|
||||
private readonly record struct LostDeadlineEntry(
|
||||
|
|
@ -770,6 +783,87 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C0-1: publishes the initial-create continuation executor's own
|
||||
/// completion (its FIFO drain has finished and its residence lease is
|
||||
/// already released) on the SAME ordered receipt stream every
|
||||
/// Place/Withdraw/Discard uses - the pinned contract forbids a second
|
||||
/// stream/queue. This is NOT an Operation-backed receipt (the executor's
|
||||
/// own placement operation, if any, already committed and was
|
||||
/// acknowledged earlier in the drain - see
|
||||
/// RuntimeInitialCreateContinuationExecutor.ExecuteCore's call site) so
|
||||
/// the token is built the same way PublishProjection assembles one, but
|
||||
/// from the CANONICAL record's current authority/version/cell facts
|
||||
/// (self-consistent: nothing else can move them synchronously between
|
||||
/// the executor's release and this publish) instead of a now-gone
|
||||
/// Operation. AcknowledgeProjection's dedicated ExecutorCompleted branch
|
||||
/// treats it exactly like Discard - acknowledge-only, no operation to
|
||||
/// resume or commit against.
|
||||
/// </summary>
|
||||
internal RuntimePlacementProjectionToken PublishExecutorCompletion(
|
||||
RuntimeEntityRecord record,
|
||||
Action<RuntimePlacementProjectionToken>? beforePublish = null,
|
||||
RuntimePortalPlacementAuthority portal = default)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (record.Key is not { } key)
|
||||
return default;
|
||||
|
||||
PhysicsBody? body = record.PhysicsBody;
|
||||
ulong sequence = checked(++_nextProjectionSequence);
|
||||
var token = new RuntimePlacementProjectionToken(
|
||||
sequence,
|
||||
Revision: 1UL,
|
||||
key,
|
||||
record.PositionAuthorityVersion,
|
||||
record.SpatialAuthorityVersion,
|
||||
record.PlacementCommitVersion,
|
||||
_entities.SessionLifetimeVersion,
|
||||
record.FullCellId,
|
||||
_physics.ExpectedCollisionGeneration(record.FullCellId),
|
||||
portal);
|
||||
var snapshot = new RuntimePlacementProjectionSnapshot(
|
||||
token,
|
||||
RuntimePlacementProjectionKind.ExecutorCompleted,
|
||||
body?.Position ?? Vector3.Zero,
|
||||
body?.Orientation ?? Quaternion.Identity,
|
||||
body?.CellPosition.Frame.Origin ?? Vector3.Zero,
|
||||
body?.InContact ?? false,
|
||||
body?.OnWalkable ?? false);
|
||||
_pendingProjection.Add(sequence, snapshot);
|
||||
// F2: register-before-publish - beforePublish runs while the token is
|
||||
// already in _pendingProjection but before PublishPlacement's
|
||||
// synchronous observer dispatch, so a subscriber reading back the
|
||||
// executor's correlation entry from inside its OWN OnPlacement
|
||||
// callback always finds it.
|
||||
beforePublish?.Invoke(token);
|
||||
PublishPlacement(snapshot);
|
||||
return token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F2: binds the ONE notification fired when a Kind ExecutorCompleted
|
||||
/// receipt is acknowledged (mirrors
|
||||
/// RuntimeInitialCreateResidenceState.BindRetirementNotification's
|
||||
/// existing one-bound-delegate shape). The executor uses this to reap
|
||||
/// its own token-to-receipt correlation entry exactly when the receipt
|
||||
/// it correlates is consumed - never before (a host might still be
|
||||
/// mid-retry) and never left dangling after (unbounded per-completed-
|
||||
/// entity retention).
|
||||
/// </summary>
|
||||
internal void BindExecutorCompletionAcknowledgement(
|
||||
Action<RuntimeEntityKey, ulong> acknowledged)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(acknowledged);
|
||||
if (_executorCompletionAcknowledged is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The executor-completion acknowledgement notification is already bound.");
|
||||
}
|
||||
_executorCompletionAcknowledged = acknowledged;
|
||||
}
|
||||
|
||||
internal void ResetSession()
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
|
|
@ -1132,6 +1226,95 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
return RuntimeSetPositionMoverPreparationStatus.Prepared;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C0-3: chains the exact-Setup mover pipeline end-to-end for an
|
||||
/// authored placement (initial-Create or any other authored-mover
|
||||
/// operation) whose route performs SetPosition - PrepareMover /
|
||||
/// RuntimeSetPositionMoverPreparer.TryBuild /
|
||||
/// IPreparedCollisionSource.ReadSetupCollision already exist piecewise
|
||||
/// (inventory gap a); this is the missing wiring, not a behavior change.
|
||||
/// Reads the CANONICAL Setup table id from the record via
|
||||
/// <see cref="CanonicalSetupTableId"/> - the SAME field
|
||||
/// <see cref="CapturePreparationAuthority"/> already trusts - rather than
|
||||
/// a caller-supplied id, so this can never be pointed at the wrong
|
||||
/// Setup. A record with no authored Setup at all (id 0) takes retail's
|
||||
/// genuine "no Setup" dummy-sphere path
|
||||
/// (<see cref="RuntimeSetPositionMoverSetup.ResolvedAbsent"/>) instead of
|
||||
/// reading anything; a record WITH an id but an unavailable/corrupt
|
||||
/// asynchronous read yields <see cref="RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable"/>
|
||||
/// so the caller retries once the prepared-collision package lands,
|
||||
/// mirroring <see cref="RuntimeSetPositionMoverSetup"/>'s own
|
||||
/// doc-comment distinction between "not arrived yet" and "resolved
|
||||
/// absent". Dormant: internal, no production caller - a residence
|
||||
/// lease's own <c>Placement</c>/<c>Route.OperationKind</c>/
|
||||
/// <c>Route.SetPositionFlags</c> are exactly the token/kind/flags this
|
||||
/// takes.
|
||||
/// </summary>
|
||||
internal RuntimeSetPositionMoverPreparationStatus
|
||||
TryPrepareAndSubmitAuthoredPlacement(
|
||||
RuntimeEntityRecord record,
|
||||
in RuntimeEntityPlacementToken token,
|
||||
RuntimeSetPositionOperationKind operationKind,
|
||||
PhysicsSetPositionFlags flags,
|
||||
IPreparedCollisionSource collisionSource,
|
||||
double gameTime,
|
||||
out RuntimeSetPositionOutcome outcome,
|
||||
PhysicsPlacementClass placementClass = PhysicsPlacementClass.Ordinary,
|
||||
RuntimePortalPlacementAuthority portal = default,
|
||||
Vector3 line = default,
|
||||
float scatterRadiusX = 0f,
|
||||
float scatterRadiusY = 0f,
|
||||
uint scatterAttempts = 0u,
|
||||
float shadowWorldOffsetX = 0f,
|
||||
float shadowWorldOffsetY = 0f)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(collisionSource);
|
||||
outcome = default;
|
||||
|
||||
uint setupTableId = CanonicalSetupTableId(record);
|
||||
RuntimeSetPositionMoverSetup setup;
|
||||
if (setupTableId == 0u)
|
||||
{
|
||||
setup = RuntimeSetPositionMoverSetup.ResolvedAbsent;
|
||||
}
|
||||
else
|
||||
{
|
||||
PreparedCollisionReadResult<FlatSetupCollision> read =
|
||||
collisionSource.ReadSetupCollision(setupTableId);
|
||||
if (read.Status != PreparedAssetReadStatus.Loaded
|
||||
|| read.Data is null)
|
||||
{
|
||||
return RuntimeSetPositionMoverPreparationStatus
|
||||
.RetrySetupUnavailable;
|
||||
}
|
||||
setup = RuntimeSetPositionMoverSetup.Resolved(
|
||||
setupTableId, read.Data);
|
||||
}
|
||||
|
||||
var preparation = new RuntimeSetPositionMoverPreparation(
|
||||
setup,
|
||||
operationKind,
|
||||
gameTime,
|
||||
placementClass,
|
||||
flags,
|
||||
line,
|
||||
scatterRadiusX,
|
||||
scatterRadiusY,
|
||||
scatterAttempts,
|
||||
shadowWorldOffsetX,
|
||||
shadowWorldOffsetY,
|
||||
portal);
|
||||
RuntimeSetPositionMoverPreparationStatus status = PrepareMover(
|
||||
token, preparation, out RuntimeSetPositionCommand command);
|
||||
if (status != RuntimeSetPositionMoverPreparationStatus.Prepared)
|
||||
return status;
|
||||
|
||||
outcome = SubmitPreparedPlacement(token, command);
|
||||
return RuntimeSetPositionMoverPreparationStatus.Prepared;
|
||||
}
|
||||
|
||||
internal bool IsExactPreparedPlacementCurrent(
|
||||
RuntimeEntityRecord record,
|
||||
in RuntimeEntityPlacementToken token,
|
||||
|
|
@ -2313,10 +2496,24 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
{
|
||||
return false;
|
||||
}
|
||||
if (pending.Kind is RuntimePlacementProjectionKind.Discard)
|
||||
if (pending.Kind is RuntimePlacementProjectionKind.Discard
|
||||
or RuntimePlacementProjectionKind.ExecutorCompleted)
|
||||
{
|
||||
// C0-1: an ExecutorCompleted receipt is never Operation-backed
|
||||
// (see PublishExecutorCompletion) - there is nothing to resume or
|
||||
// commit against, exactly like Discard.
|
||||
_pendingProjection.Remove(token.Sequence);
|
||||
RetireQuiescenceProjectionSequence(token.Sequence);
|
||||
if (pending.Kind is RuntimePlacementProjectionKind.ExecutorCompleted)
|
||||
{
|
||||
// F2: notify the executor so it can reap its own
|
||||
// token-to-receipt correlation entry now - not before (a
|
||||
// host might still be mid-retry against this exact
|
||||
// unacknowledged receipt) and not left dangling after.
|
||||
_executorCompletionAcknowledged?.Invoke(
|
||||
token.Entity,
|
||||
token.Sequence);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!_operations.TryGetValue(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue