feat(runtime): prepare authored SetPosition movers

This commit is contained in:
Erik 2026-08-01 09:16:09 +02:00
parent 237d1184d2
commit 442cb8f97b
8 changed files with 1516 additions and 70 deletions

View file

@ -57,7 +57,17 @@ public sealed class RuntimeSetPositionStateTests
record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative,
invalidPortal);
Assert.True(portalToken.IsValid);
Assert.False(portalToken.IsValid);
var matchingPortal = invalidPortal with
{
Projection = new RuntimeWorldHostProjectionToken(7, SourceCell),
};
Assert.True(lifetime.Physics.SetPosition.BeginAuthoredPlacement(
record,
record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative,
matchingPortal).IsValid);
}
[Fact]
@ -445,6 +455,146 @@ public sealed class RuntimeSetPositionStateTests
placed.Token));
}
[Fact]
public void LegacyDeferredWakeCannotBypassNewAuthoredSealAfterAuthorityStales()
{
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
using var lifetime = new RuntimeEntityObjectLifetime(engine);
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000112Bu, 1);
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
var observer = new PlacementObserver();
using IDisposable subscription = lifetime.Events.SubscribePlacement(
observer);
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
record,
record.PositionAuthorityVersion,
Command(CrossLandblockRequest()));
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
deferred.Projection));
// The direct legacy command was valid when parked, but any accepted
// authority replacement requires a new authored mover preparation.
lifetime.Entities.AdvanceObjDescAuthority(record);
AddFlatLandblock(engine, DestinationLandblock, 192f);
lifetime.Physics.SetPosition.CommitCollisionGeneration(
DestinationLandblock,
generation: 1,
ready: true);
Assert.False(body.InWorld);
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
record,
out RuntimeEntityPlacementToken token));
Assert.Equal(RuntimeEntityPlacementPreparationKind.LegacyDirect,
token.PreparationKind);
RuntimeSetPositionOutcome bypass = lifetime.Physics.SetPosition
.SubmitPreparedPlacement(token, Command(CrossLandblockRequest()));
Assert.Equal(RuntimeSetPositionStatus.Rejected, bypass.Status);
Assert.False(body.InWorld);
RuntimeSetPositionCommand prepared = PrepareAuthoredCommand(
lifetime,
token,
[new FlatCollisionSphere(Vector3.Zero, 0.4f)]);
RuntimeSetPositionOutcome resumed = lifetime.Physics.SetPosition
.SubmitPreparedPlacement(token, prepared);
Assert.Equal(PhysicsResidenceDisposition.Committed, resumed.Residence);
RuntimePlacementProjectionSnapshot placed = observer.Deltas[^1].Placement;
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
Assert.True(body.InWorld);
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
placed.Token));
}
[Theory]
[InlineData(DeferredPhysicsStateMutation.ChildNoDraw)]
[InlineData(DeferredPhysicsStateMutation.StopMissile)]
[InlineData(DeferredPhysicsStateMutation.DirectFinalState)]
public void DeferredWakeRequiresRepreparationAfterFinalPhysicsStateMutation(
DeferredPhysicsStateMutation mutation)
{
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
using var lifetime = new RuntimeEntityObjectLifetime(engine);
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000112Cu, 1);
PhysicsStateFlags initial = PhysicsStateFlags.Gravity;
if (mutation is DeferredPhysicsStateMutation.StopMissile)
{
initial |= PhysicsStateFlags.Missile
| PhysicsStateFlags.AlignPath
| PhysicsStateFlags.PathClipped;
}
PhysicsBody body = AttachBody(lifetime, record, SourceCell, initial);
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
record,
record.PositionAuthorityVersion,
Command(CrossLandblockRequest()));
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
deferred.Projection));
ulong priorMutation = record.PhysicsStateMutationVersion;
switch (mutation)
{
case DeferredPhysicsStateMutation.ChildNoDraw:
lifetime.Entities.SetChildNoDraw(record, noDraw: true);
break;
case DeferredPhysicsStateMutation.StopMissile:
Assert.True(lifetime.Entities.StopMissileAfterCollision(
record,
requireCurrentMissile: true));
break;
case DeferredPhysicsStateMutation.DirectFinalState:
lifetime.Entities.SetFinalPhysicsState(
record,
record.FinalPhysicsState | PhysicsStateFlags.Frozen);
break;
default:
throw new ArgumentOutOfRangeException(nameof(mutation));
}
Assert.Equal(priorMutation + 1UL,
record.PhysicsStateMutationVersion);
if (mutation is DeferredPhysicsStateMutation.DirectFinalState)
Assert.NotEqual(record.FinalPhysicsState, body.State);
else
Assert.Equal(record.FinalPhysicsState, body.State);
AddFlatLandblock(engine, DestinationLandblock, 192f);
lifetime.Physics.SetPosition.CommitCollisionGeneration(
DestinationLandblock,
generation: 1,
ready: true);
Assert.False(body.InWorld);
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
record,
out _));
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
.AwaitingSetPositionPreparationCount);
}
[Fact]
public void DirectFinalPhysicsStateMutationAdvancesOnlyOnChangeAndLeavesBodyToItsOwner()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000112Du, 1);
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
PhysicsStateFlags original = record.FinalPhysicsState;
ulong version = record.PhysicsStateMutationVersion;
lifetime.Entities.SetFinalPhysicsState(record, original);
Assert.Equal(version, record.PhysicsStateMutationVersion);
Assert.Equal(original, body.State);
PhysicsStateFlags changed = original | PhysicsStateFlags.Frozen;
lifetime.Entities.SetFinalPhysicsState(record, changed);
Assert.Equal(version + 1UL, record.PhysicsStateMutationVersion);
Assert.Equal(changed, record.FinalPhysicsState);
Assert.Equal(original, body.State);
lifetime.Entities.SetFinalPhysicsState(record, changed);
Assert.Equal(version + 1UL, record.PhysicsStateMutationVersion);
Assert.Equal(original, body.State);
}
[Fact]
public void RejectedPreparationKeepsValidatedMoverAndExactTokenRetryable()
{
@ -1943,15 +2093,12 @@ public sealed class RuntimeSetPositionStateTests
new FlatCollisionSphere(new Vector3(0.4f, 0f, 0f), 0.35f),
new FlatCollisionSphere(new Vector3(-0.3f, 0f, 0.5f), 0.2f),
];
RuntimeSetPositionCommand preparedCommand = PrepareAuthoredCommand(
lifetime,
token,
authored);
RuntimeSetPositionOutcome supplied = lifetime.Physics.SetPosition
.SubmitPreparedPlacement(
token,
Command(Request(
landblock | 0x0101u,
new Vector3(999f, 999f, 999f)) with
{
Spheres = authored,
}));
.SubmitPreparedPlacement(token, preparedCommand);
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, supplied.Status);
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
@ -1998,21 +2145,29 @@ public sealed class RuntimeSetPositionStateTests
Assert.Single(observer.Deltas).Placement.Token));
observer.Deltas.Clear();
RuntimeSetPositionCommand malformed = Command(Request(
exactCell,
new Vector3(10f, 18f, 7f)) with
{
Spheres =
[
new FlatCollisionSphere(
new Vector3(float.NaN, 0f, 0f),
0.4f),
],
});
RuntimeSetPositionOutcome acceptedMalformed = lifetime.Physics
.SetPosition.SubmitPreparedPlacement(token, malformed);
Assert.Equal(RuntimeSetPositionStatus.DeferredCell,
acceptedMalformed.Status);
var malformedSetup = new FlatSetupCollision(
ImmutableArray<FlatCollisionCylinder>.Empty,
[new FlatCollisionSphere(
new Vector3(float.NaN, 0f, 0f),
0.4f)],
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f);
Assert.Equal(
RuntimeSetPositionMoverPreparationStatus.InvalidData,
lifetime.Physics.SetPosition.PrepareMover(
token,
new RuntimeSetPositionMoverPreparation(
RuntimeSetPositionMoverSetup.Resolved(
0x02000001u,
malformedSetup),
RuntimeSetPositionOperationKind.RemoteAuthoritative,
GameTime: 10d,
PhysicsPlacementClass.Ordinary,
PhysicsSetPositionFlags.Placement
| PhysicsSetPositionFlags.Slide),
out _));
RuntimeCollisionAdmission first = lifetime.Physics
.BeginCollisionAdmission(landblock);
@ -2032,44 +2187,25 @@ public sealed class RuntimeSetPositionStateTests
RuntimeSetPositionOwnershipSnapshot invalidWake = lifetime.Physics
.SetPosition.CaptureOwnership();
Assert.Equal(1, invalidWake.AwaitingPreparationCount);
Assert.Equal(1, invalidWake.DeferredBucketCount);
Assert.Equal(0, invalidWake.DeferredBucketCount);
Assert.Equal(1, invalidWake.LostDeadlineCount);
Assert.Empty(observer.Deltas);
Assert.False(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
record,
out _));
RuntimeSetPositionCommand corrected = Command(Request(
exactCell,
new Vector3(10f, 18f, 7f)) with
{
Spheres =
[
new FlatCollisionSphere(Vector3.Zero, 0.4f),
],
});
RuntimeSetPositionCommand corrected = PrepareAuthoredCommand(
lifetime,
token,
[new FlatCollisionSphere(Vector3.Zero, 0.4f)]);
RuntimeSetPositionOutcome correctedPending = lifetime.Physics
.SetPosition.SubmitPreparedPlacement(token, corrected);
Assert.Equal(RuntimeSetPositionStatus.DeferredCell,
correctedPending.Status);
Assert.Equal(PhysicsResidenceDisposition.DeferredCell,
Assert.Equal(PhysicsResidenceDisposition.Committed,
correctedPending.Residence);
Assert.Equal(exactCell, correctedPending.ExactCellId);
RuntimeCollisionAdmission second = lifetime.Physics
.BeginCollisionAdmission(landblock);
using PreparedLandblockCollisionGeneration preparedSecond = lifetime
.Physics.PrepareCollisionGeneration(second);
lifetime.Physics.StageCollisionAssets(
second,
preparedSecond,
CollisionAssets(landblock));
AddSyntheticCell(preparedSecond.DataCache, exactCell);
Assert.True(CommitPrepared(
lifetime.Physics,
second,
preparedSecond).Committed);
RuntimePlacementProjectionSnapshot placed = Assert.Single(observer.Deltas)
.Placement;
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
@ -2178,6 +2314,34 @@ public sealed class RuntimeSetPositionStateTests
ShadowWorldOffsetX: request.CellId == SourceCell ? 0f : 192f,
ShadowWorldOffsetY: 0f);
private static RuntimeSetPositionCommand PrepareAuthoredCommand(
RuntimeEntityObjectLifetime lifetime,
RuntimeEntityPlacementToken token,
ImmutableArray<FlatCollisionSphere> spheres)
{
var setup = new FlatSetupCollision(
ImmutableArray<FlatCollisionCylinder>.Empty,
spheres,
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f);
var preparation = new RuntimeSetPositionMoverPreparation(
RuntimeSetPositionMoverSetup.Resolved(0x02000001u, setup),
RuntimeSetPositionOperationKind.RemoteAuthoritative,
GameTime: 10d,
PhysicsPlacementClass.Ordinary,
PhysicsSetPositionFlags.Placement
| PhysicsSetPositionFlags.Slide);
Assert.Equal(
RuntimeSetPositionMoverPreparationStatus.Prepared,
lifetime.Physics.SetPosition.PrepareMover(
token,
preparation,
out RuntimeSetPositionCommand command));
return command;
}
private static PhysicsSetPositionRequest CrossLandblockRequest() =>
Request(
SourceCell,
@ -2480,6 +2644,13 @@ public sealed class RuntimeSetPositionStateTests
internal void JumpUtc(TimeSpan duration) => _utcNow += duration;
}
public enum DeferredPhysicsStateMutation
{
ChildNoDraw,
StopMissile,
DirectFinalState,
}
private enum CancellationChannel : uint
{
Position,