feat(headless): observe canonical placement receipts
This commit is contained in:
parent
74c9b155bd
commit
378ca95a67
5 changed files with 445 additions and 23 deletions
|
|
@ -374,6 +374,231 @@ public sealed class HeadlessSessionHostTests
|
|||
Assert.Equal(2, collision.CenterCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlacementReceiptValidationDoesNotRegainMovementOrPhysicsAuthority()
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
const uint player = 0x50000004u;
|
||||
runtime.PlayerIdentity.ServerGuid = player;
|
||||
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
||||
RuntimeEntityRecord record = runtime.EntityObjects
|
||||
.RegisterEntity(Spawn(player))
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
record,
|
||||
record.CreateIntegrationVersion,
|
||||
record.Snapshot,
|
||||
replaceGeneration: false));
|
||||
var directProjection = new HeadlessSessionWorldProjection(
|
||||
runtime,
|
||||
new FixtureCollisionNeighborhood());
|
||||
directProjection.ProjectSpawn(record, isLocalPlayer: true);
|
||||
PlayerMovementController controller = Assert.IsType<
|
||||
PlayerMovementController>(runtime.MovementOwner.Controller);
|
||||
Vector3 positionBefore = controller.Position;
|
||||
Quaternion orientationBefore = controller.BodyOrientation;
|
||||
RuntimePhysicsOwnershipSnapshot physicsBefore =
|
||||
runtime.EntityObjects.Physics.CaptureOwnership();
|
||||
RuntimePlacementProjectionSnapshot receipt = Placement(
|
||||
runtime,
|
||||
record,
|
||||
RuntimePlacementProjectionKind.Place,
|
||||
new Vector3(600f, 601f, 602f),
|
||||
Quaternion.CreateFromAxisAngle(Vector3.UnitY, 1.2f));
|
||||
|
||||
var receiptSink = new HeadlessRuntimePlacementProjectionSink(runtime);
|
||||
Assert.True(receiptSink.TryApply(in receipt));
|
||||
|
||||
Assert.Equal(positionBefore, controller.Position);
|
||||
Assert.Equal(orientationBefore, controller.BodyOrientation);
|
||||
Assert.Equal(
|
||||
physicsBefore,
|
||||
runtime.EntityObjects.Physics.CaptureOwnership());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlacementReceiptUsesExactIncarnationAndDiscardIsAckOnly()
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
RuntimeEntityRecord record = runtime.EntityObjects
|
||||
.RegisterEntity(Spawn(0x50000005u))
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
record,
|
||||
record.CreateIntegrationVersion,
|
||||
record.Snapshot,
|
||||
replaceGeneration: false));
|
||||
var projection = new HeadlessRuntimePlacementProjectionSink(runtime);
|
||||
RuntimePlacementProjectionSnapshot place = Placement(
|
||||
runtime,
|
||||
record,
|
||||
RuntimePlacementProjectionKind.Place,
|
||||
Vector3.One,
|
||||
Quaternion.Identity);
|
||||
RuntimePlacementProjectionSnapshot stale = place with
|
||||
{
|
||||
Token = place.Token with
|
||||
{
|
||||
Entity = place.Token.Entity with
|
||||
{
|
||||
Incarnation = unchecked((ushort)(
|
||||
place.Token.Entity.Incarnation + 1)),
|
||||
},
|
||||
},
|
||||
};
|
||||
RuntimePlacementProjectionSnapshot discard = stale with
|
||||
{
|
||||
Kind = RuntimePlacementProjectionKind.Discard,
|
||||
Token = stale.Token with
|
||||
{
|
||||
SessionLifetimeVersion = ulong.MaxValue,
|
||||
},
|
||||
};
|
||||
|
||||
Assert.False(projection.TryApply(in stale));
|
||||
Assert.True(projection.TryApply(in discard));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionEventRouteOwnsOneObserverAndUnsubscribesBeforeNetworkDetach()
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
int subscriberCountDuringDetach = -1;
|
||||
var inner = new FixtureEventRoute(
|
||||
onDispose: () => subscriberCountDuringDetach =
|
||||
runtime.EntityObjects.Events.PlacementSubscriberCount);
|
||||
var placements = new HeadlessRuntimePlacementProjectionSink(runtime);
|
||||
var route = new HeadlessSessionEventRoute(
|
||||
inner,
|
||||
runtime,
|
||||
placements);
|
||||
|
||||
Assert.Equal(
|
||||
0,
|
||||
runtime.EntityObjects.Events.PlacementSubscriberCount);
|
||||
route.Attach();
|
||||
route.Attach();
|
||||
Assert.Equal(
|
||||
1,
|
||||
runtime.EntityObjects.Events.PlacementSubscriberCount);
|
||||
Assert.Equal(1, inner.AttachCount);
|
||||
|
||||
route.Dispose();
|
||||
route.Dispose();
|
||||
|
||||
Assert.Equal(0, subscriberCountDuringDetach);
|
||||
Assert.Equal(
|
||||
0,
|
||||
runtime.EntityObjects.Events.PlacementSubscriberCount);
|
||||
Assert.Equal(1, inner.DisposeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplacementSessionEventRouteGetsOneFreshObserver()
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
var placements = new HeadlessRuntimePlacementProjectionSink(runtime);
|
||||
|
||||
var first = new HeadlessSessionEventRoute(
|
||||
new FixtureEventRoute(),
|
||||
runtime,
|
||||
placements);
|
||||
first.Attach();
|
||||
Assert.Equal(
|
||||
1,
|
||||
runtime.EntityObjects.Events.PlacementSubscriberCount);
|
||||
first.Dispose();
|
||||
Assert.Equal(
|
||||
0,
|
||||
runtime.EntityObjects.Events.PlacementSubscriberCount);
|
||||
|
||||
var replacement = new HeadlessSessionEventRoute(
|
||||
new FixtureEventRoute(),
|
||||
runtime,
|
||||
placements);
|
||||
replacement.Attach();
|
||||
Assert.Equal(
|
||||
1,
|
||||
runtime.EntityObjects.Events.PlacementSubscriberCount);
|
||||
replacement.Dispose();
|
||||
Assert.Equal(
|
||||
0,
|
||||
runtime.EntityObjects.Events.PlacementSubscriberCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionEventRouteRetryDoesNotRestorePlacementObserver()
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
var inner = new FixtureEventRoute
|
||||
{
|
||||
DisposeFailuresRemaining = 1,
|
||||
};
|
||||
var route = new HeadlessSessionEventRoute(
|
||||
inner,
|
||||
runtime,
|
||||
new HeadlessRuntimePlacementProjectionSink(runtime));
|
||||
route.Attach();
|
||||
|
||||
Assert.Throws<IOException>(route.Dispose);
|
||||
Assert.Equal(
|
||||
0,
|
||||
runtime.EntityObjects.Events.PlacementSubscriberCount);
|
||||
Assert.Equal(1, inner.DisposeCount);
|
||||
|
||||
route.Dispose();
|
||||
|
||||
Assert.Equal(2, inner.DisposeCount);
|
||||
Assert.Equal(
|
||||
0,
|
||||
runtime.EntityObjects.Events.PlacementSubscriberCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollisionTransactionCancelsPostAdmissionFaultWithoutWithdrawingActiveWorld()
|
||||
{
|
||||
|
|
@ -587,6 +812,36 @@ public sealed class HeadlessSessionHostTests
|
|||
Physics: physics);
|
||||
}
|
||||
|
||||
private static RuntimePlacementProjectionSnapshot Placement(
|
||||
GameRuntime runtime,
|
||||
RuntimeEntityRecord record,
|
||||
RuntimePlacementProjectionKind kind,
|
||||
Vector3 position,
|
||||
Quaternion orientation)
|
||||
{
|
||||
RuntimeEntityKey key = Assert.IsType<RuntimeEntityKey>(record.Key);
|
||||
var token = new RuntimePlacementProjectionToken(
|
||||
Sequence: 1,
|
||||
Revision: 1,
|
||||
Entity: key,
|
||||
PositionAuthorityVersion: record.PositionAuthorityVersion,
|
||||
SpatialAuthorityVersion: record.SpatialAuthorityVersion,
|
||||
PlacementCommitVersion: record.PlacementCommitVersion,
|
||||
SessionLifetimeVersion:
|
||||
runtime.EntityObjects.Entities.SessionLifetimeVersion,
|
||||
ExactCellId: record.FullCellId,
|
||||
CollisionGeneration: 1,
|
||||
Portal: default);
|
||||
return new RuntimePlacementProjectionSnapshot(
|
||||
token,
|
||||
kind,
|
||||
position,
|
||||
orientation,
|
||||
CellLocalPosition: position,
|
||||
InContact: false,
|
||||
OnWalkable: false);
|
||||
}
|
||||
|
||||
private static uint ActionOpcode(byte[] body) =>
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
body.AsSpan(8, sizeof(uint)));
|
||||
|
|
@ -687,4 +942,25 @@ public sealed class HeadlessSessionHostTests
|
|||
fullCellId == LastCell;
|
||||
}
|
||||
|
||||
private sealed class FixtureEventRoute(
|
||||
Action? onDispose = null) : ILiveSessionEventRouting
|
||||
{
|
||||
public int AttachCount { get; private set; }
|
||||
public int DisposeCount { get; private set; }
|
||||
public int DisposeFailuresRemaining { get; set; }
|
||||
|
||||
public void Attach() => AttachCount++;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeCount++;
|
||||
onDispose?.Invoke();
|
||||
if (DisposeFailuresRemaining > 0)
|
||||
{
|
||||
DisposeFailuresRemaining--;
|
||||
throw new IOException("fixture route detach failure");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue