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,7 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
using AcDream.Content.Pak;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
|
@ -2247,6 +2249,263 @@ public sealed class RuntimeSetPositionStateTests
|
|||
placed.Token));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// C0-3: exact-Setup mover chain end-to-end
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void TryPrepareAndSubmitAuthoredPlacement_ChainsSetupReadThroughPrepareMoverToSubmit()
|
||||
{
|
||||
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
||||
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
||||
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001101u, 1);
|
||||
AttachBody(lifetime, record, SourceCell);
|
||||
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
||||
.BeginAuthoredPlacement(
|
||||
record,
|
||||
record.PositionAuthorityVersion,
|
||||
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
||||
Assert.True(token.IsValid);
|
||||
|
||||
// Spawn's own default SetupTableId (0x02000001u) - the exact
|
||||
// CanonicalSetupTableId CapturePreparationAuthority already trusts;
|
||||
// the chain must read THIS id, not a caller-supplied one.
|
||||
ImmutableArray<FlatCollisionSphere> spheres =
|
||||
[
|
||||
new FlatCollisionSphere(new Vector3(0f, 0f, 0.5f), 0.4f),
|
||||
new FlatCollisionSphere(new Vector3(0f, 0f, 1.2f), 0.4f),
|
||||
];
|
||||
var source = new FakeCollisionSource(
|
||||
0x02000001u,
|
||||
new FlatSetupCollision(
|
||||
ImmutableArray<FlatCollisionCylinder>.Empty,
|
||||
spheres,
|
||||
height: 0f,
|
||||
radius: 0f,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.35f));
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||
lifetime.Physics.SetPosition.TryPrepareAndSubmitAuthoredPlacement(
|
||||
record,
|
||||
token,
|
||||
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
||||
PhysicsSetPositionFlags.Placement
|
||||
| PhysicsSetPositionFlags.Slide,
|
||||
source,
|
||||
gameTime: 10d,
|
||||
out RuntimeSetPositionOutcome outcome));
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
||||
outcome.Status);
|
||||
Assert.Equal(1, source.ReadCount);
|
||||
Assert.True(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
||||
record,
|
||||
out int sphereCount));
|
||||
Assert.Equal(2, sphereCount);
|
||||
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
||||
outcome.Projection));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPrepareAndSubmitAuthoredPlacement_YieldsRetryOnAMissingSetupReadWithoutMutatingStage()
|
||||
{
|
||||
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
||||
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
||||
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001102u, 1);
|
||||
AttachBody(lifetime, record, SourceCell);
|
||||
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
||||
.BeginAuthoredPlacement(
|
||||
record,
|
||||
record.PositionAuthorityVersion,
|
||||
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
||||
Assert.True(token.IsValid);
|
||||
|
||||
var source = new FakeCollisionSource(
|
||||
0x02000001u,
|
||||
setup: null,
|
||||
status: PreparedAssetReadStatus.Missing);
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable,
|
||||
lifetime.Physics.SetPosition.TryPrepareAndSubmitAuthoredPlacement(
|
||||
record,
|
||||
token,
|
||||
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
||||
PhysicsSetPositionFlags.Placement
|
||||
| PhysicsSetPositionFlags.Slide,
|
||||
source,
|
||||
gameTime: 10d,
|
||||
out RuntimeSetPositionOutcome outcome));
|
||||
|
||||
Assert.Equal(default, outcome);
|
||||
Assert.Equal(1, source.ReadCount);
|
||||
// Never manufactured a fallback while the read is in flight - the
|
||||
// token can still be prepared once the asset lands.
|
||||
Assert.True(lifetime.Physics.SetPosition.IsPlacementCurrent(token));
|
||||
Assert.False(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
||||
record,
|
||||
out _));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// C0-1: executor completion on the SAME ordered placement stream
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void PublishExecutorCompletion_PublishesAcknowledgeOnlyReceiptAndConverges()
|
||||
{
|
||||
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
||||
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
||||
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001201u, 1);
|
||||
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
||||
var observer = new PlacementObserver();
|
||||
using IDisposable subscription =
|
||||
lifetime.Events.SubscribePlacement(observer);
|
||||
|
||||
RuntimePlacementProjectionToken token =
|
||||
lifetime.Physics.SetPosition.PublishExecutorCompletion(record);
|
||||
|
||||
Assert.True(token.IsValid);
|
||||
Assert.Equal(record.Key, token.Entity);
|
||||
RuntimePlacementProjectionSnapshot published =
|
||||
Assert.Single(observer.Deltas).Placement;
|
||||
Assert.Equal(RuntimePlacementProjectionKind.ExecutorCompleted,
|
||||
published.Kind);
|
||||
Assert.Equal(token, published.Token);
|
||||
Assert.Equal(body.Position, published.WorldPosition);
|
||||
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||
|
||||
// Acknowledge-only, exactly like Discard - no operation to resume.
|
||||
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(token));
|
||||
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||
// A stale re-acknowledge of the already-consumed receipt fails.
|
||||
Assert.False(lifetime.Physics.SetPosition.AcknowledgeProjection(token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublishExecutorCompletion_RespectsExactHeadOrderingAcrossEntities()
|
||||
{
|
||||
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
||||
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
||||
RuntimeEntityRecord first = CreateRecord(lifetime, 0x70001202u, 1);
|
||||
AttachBody(lifetime, first, SourceCell);
|
||||
RuntimeEntityRecord second = CreateRecord(lifetime, 0x70001203u, 1);
|
||||
AttachBody(lifetime, second, SourceCell);
|
||||
|
||||
RuntimePlacementProjectionToken firstToken =
|
||||
lifetime.Physics.SetPosition.PublishExecutorCompletion(first);
|
||||
RuntimePlacementProjectionToken secondToken =
|
||||
lifetime.Physics.SetPosition.PublishExecutorCompletion(second);
|
||||
|
||||
Assert.Equal(2, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||
// The exact head (first) must acknowledge before the second, matching
|
||||
// the ordered-stream contract every other Kind already honors.
|
||||
Assert.False(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
||||
secondToken));
|
||||
Assert.Equal(2, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
||||
firstToken));
|
||||
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
||||
secondToken));
|
||||
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// C0-4: TryCommitParent/CommitWithdrawal cancellation asymmetries
|
||||
// (the ordinary Physics.SetPosition.Forget half, isolated from any
|
||||
// initial-create residence)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void TryCommitParent_CancelsASeparateActiveOrdinaryPendingPlacement()
|
||||
{
|
||||
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
||||
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
||||
const uint guid = 0x70001301u;
|
||||
RuntimeEntityRecord record = CreateRecord(lifetime, guid, 1);
|
||||
AttachBody(lifetime, record, SourceCell);
|
||||
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
||||
record,
|
||||
record.PositionAuthorityVersion,
|
||||
Command(Request(SourceCell, new Vector3(12f, 18f, 7f))));
|
||||
Assert.Equal(
|
||||
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
||||
outcome.Status);
|
||||
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||
|
||||
var discards = new List<RuntimePlacementProjectionSnapshot>();
|
||||
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
||||
new PlacementObserver(delta =>
|
||||
{
|
||||
if (delta.Placement.Kind is RuntimePlacementProjectionKind.Discard)
|
||||
discards.Add(delta.Placement);
|
||||
}));
|
||||
|
||||
var relation = new ParentAttachmentRelation(
|
||||
ParentGuid: 0x70001400u,
|
||||
ChildGuid: guid,
|
||||
ParentLocation: 1u,
|
||||
PlacementId: 1u,
|
||||
ParentInstanceSequence: 1,
|
||||
ChildPositionSequence: 1);
|
||||
Assert.True(lifetime.TryCommitParent(relation, null, out _));
|
||||
|
||||
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership()
|
||||
.ActiveOperationCount);
|
||||
// The old Place receipt is REPLACED by a Discard at the same
|
||||
// sequence, not removed outright - it still awaits host
|
||||
// acknowledgement, exactly like every other cancelled-in-flight
|
||||
// receipt.
|
||||
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||
RuntimePlacementProjectionSnapshot discard = Assert.Single(discards);
|
||||
Assert.Equal(record.Key, discard.Token.Entity);
|
||||
Assert.Equal(outcome.Projection.Sequence, discard.Token.Sequence);
|
||||
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
||||
discard.Token));
|
||||
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommitWithdrawal_CancelsAnActiveOrdinaryPendingPlacementSymmetricallyWithPickup()
|
||||
{
|
||||
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
||||
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
||||
const uint guid = 0x70001302u;
|
||||
RuntimeEntityRecord record = CreateRecord(lifetime, guid, 1);
|
||||
AttachBody(lifetime, record, SourceCell);
|
||||
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
||||
record,
|
||||
record.PositionAuthorityVersion,
|
||||
Command(Request(SourceCell, new Vector3(12f, 18f, 7f))));
|
||||
Assert.Equal(
|
||||
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
||||
outcome.Status);
|
||||
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||
|
||||
var discards = new List<RuntimePlacementProjectionSnapshot>();
|
||||
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
||||
new PlacementObserver(delta =>
|
||||
{
|
||||
if (delta.Placement.Kind is RuntimePlacementProjectionKind.Discard)
|
||||
discards.Add(delta.Placement);
|
||||
}));
|
||||
|
||||
Assert.True(lifetime.CommitWithdrawal(record));
|
||||
|
||||
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership()
|
||||
.ActiveOperationCount);
|
||||
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||
RuntimePlacementProjectionSnapshot discard = Assert.Single(discards);
|
||||
Assert.Equal(record.Key, discard.Token.Entity);
|
||||
Assert.Equal(outcome.Projection.Sequence, discard.Token.Sequence);
|
||||
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
||||
discard.Token));
|
||||
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||
}
|
||||
|
||||
private static void VerifyPositionChannelCancellation(
|
||||
CancellationChannel channel)
|
||||
{
|
||||
|
|
@ -2618,6 +2877,73 @@ public sealed class RuntimeSetPositionStateTests
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C0-3 test double: a minimal <see cref="IPreparedCollisionSource"/>
|
||||
/// serving exactly one Setup id (matching <c>CanonicalSetupTableId</c>'s
|
||||
/// field), so <see cref="TryPrepareAndSubmitAuthoredPlacement_ChainsSetupReadThroughPrepareMoverToSubmit"/>
|
||||
/// can prove <c>ReadSetupCollision</c> -> <c>PrepareMover</c> ->
|
||||
/// <c>SubmitPreparedPlacement</c> actually chains, not merely that each
|
||||
/// step works in isolation (the existing preparer tests' coverage).
|
||||
/// </summary>
|
||||
private sealed class FakeCollisionSource(
|
||||
uint expectedSetupTableId,
|
||||
FlatSetupCollision? setup,
|
||||
PreparedAssetReadStatus status = PreparedAssetReadStatus.Loaded)
|
||||
: IPreparedCollisionSource
|
||||
{
|
||||
internal int ReadCount { get; private set; }
|
||||
|
||||
public PreparedAssetPresence ProbeCollision(
|
||||
PakAssetType type, uint sourceFileId) =>
|
||||
PreparedAssetPresence.Available;
|
||||
|
||||
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||
ReadGfxObjCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException(
|
||||
"Only ReadSetupCollision is exercised by C0-3.");
|
||||
|
||||
public PreparedCollisionReadResult<FlatSetupCollision>
|
||||
ReadSetupCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ReadCount++;
|
||||
Assert.Equal(expectedSetupTableId, sourceFileId);
|
||||
return status switch
|
||||
{
|
||||
PreparedAssetReadStatus.Loaded when setup is not null =>
|
||||
PreparedCollisionReadResult<FlatSetupCollision>.Loaded(
|
||||
setup),
|
||||
PreparedAssetReadStatus.Corrupt =>
|
||||
PreparedCollisionReadResult<FlatSetupCollision>.Corrupt,
|
||||
_ => PreparedCollisionReadResult<FlatSetupCollision>.Missing,
|
||||
};
|
||||
}
|
||||
|
||||
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
|
||||
ReadCellStructureCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException(
|
||||
"Only ReadSetupCollision is exercised by C0-3.");
|
||||
|
||||
public PreparedCollisionReadResult<FlatEnvCellTopology>
|
||||
ReadEnvCellTopology(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException(
|
||||
"Only ReadSetupCollision is exercised by C0-3.");
|
||||
|
||||
public PreparedCollisionSourceStats CollisionStats =>
|
||||
new(ReadCount, ReadCount, ReadCount, 0, 0);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CollisionReportObserver
|
||||
: IRuntimeCollisionReportObserver
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue