acdream/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs
Erik 67f63e85e5 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>
2026-08-02 05:22:37 +02:00

1243 lines
43 KiB
C#

using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Net;
using System.Numerics;
using System.Reflection;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Headless.Configuration;
using AcDream.Headless.Credentials;
using AcDream.Headless.Diagnostics;
using AcDream.Headless.Hosting;
using AcDream.Headless.Platform;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using AcDream.Runtime.World;
namespace AcDream.Headless.Tests;
public sealed class HeadlessSessionHostTests
{
[Fact]
public void SingleSessionStartsReconnectsAndConvergesWithoutPresentation()
{
var operations = new FixtureSessionOperations();
using var diagnosticsOutput = new StringWriter();
using var credential = new HeadlessCredentialSecret(
"fixture",
"password");
var host = new HeadlessSessionHost(
Descriptor(),
credential,
new HeadlessDiagnosticWriter(diagnosticsOutput),
operations);
RuntimeSessionStartResult first = host.Start();
ulong firstGeneration = host.Runtime.Generation.Value;
host.Tick(0.015d);
RuntimeSessionStartResult second = host.Reconnect();
Assert.Equal(RuntimeSessionStartStatus.Connected, first.Status);
Assert.Equal(RuntimeSessionStartStatus.Connected, second.Status);
Assert.Equal(0x50000002u, first.CharacterId);
Assert.Equal("Headless", host.ActiveCharacterName);
Assert.True(host.Runtime.Session.IsInWorld);
Assert.True(host.Runtime.Generation.Value > firstGeneration);
Assert.Equal(2, operations.CreatedSessionCount);
Assert.Equal(1, operations.DisposedSessionCount);
host.Dispose();
Assert.Equal(2, operations.DisposedSessionCount);
Assert.True(host.Runtime.CaptureOwnership().IsConverged);
Assert.True(credential.IsDisposed);
string diagnostics = diagnosticsOutput.ToString();
Assert.Contains("\"state\":\"start-result\"", diagnostics);
Assert.DoesNotContain("password", diagnostics);
Assert.DoesNotContain("AcDream.App", diagnostics);
}
[Fact]
public async Task ProcessHostRunsUntilCancellationAndReturnsStableExitCode()
{
var configuration = new HeadlessConfiguration
{
Version = 1,
Sessions =
[
Descriptor(
HeadlessCredentialProviderKind.StandardInput,
"stdin-bot"),
],
};
HeadlessPathSet paths = HeadlessPathSet.Resolve(
new HeadlessPathOverrides());
using var diagnostics = new StringWriter();
var operations = new FixtureSessionOperations();
using var host = new HeadlessProcessHost(
configuration,
paths,
new System.IO.StringReader(
"process-password" + Environment.NewLine),
diagnostics,
operations);
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
HeadlessExitCode result =
await host.RunAsync(cancellation.Token);
Assert.Equal(HeadlessExitCode.Success, result);
Assert.True(host.Session.Runtime.Session.IsInWorld);
Assert.DoesNotContain(
"process-password",
diagnostics.ToString());
}
[Fact]
public async Task DirectCredentialsOverrideSingleConfiguredSession()
{
var configuration = new HeadlessConfiguration
{
Version = 1,
Sessions = [Descriptor()],
};
HeadlessPathSet paths = HeadlessPathSet.Resolve(
new HeadlessPathOverrides());
using var diagnostics = new StringWriter();
var operations = new FixtureSessionOperations();
using var host = new HeadlessProcessHost(
configuration,
paths,
TextReader.Null,
diagnostics,
operations,
directCredentials: new HeadlessDirectCredentials(
"direct-account",
"direct-secret"));
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
HeadlessExitCode result =
await host.RunAsync(cancellation.Token);
Assert.Equal(HeadlessExitCode.Success, result);
Assert.Equal("direct-account", operations.LastUser);
Assert.Equal("direct-secret", operations.LastPassword);
Assert.DoesNotContain(
"direct-secret",
diagnostics.ToString());
}
[Fact]
public void DirectFrameUsesSharedRetailOrderAndMovementCadence()
{
var operations = new FixtureSessionOperations();
using var credential = new HeadlessCredentialSecret(
"fixture",
"password");
using var host = new HeadlessSessionHost(
Descriptor(),
credential,
new HeadlessDiagnosticWriter(TextWriter.Null),
operations);
Assert.Equal(
RuntimeSessionStartStatus.Connected,
host.Start().Status);
HydrateGroundedPlayer(host.Runtime);
var sent = new List<(byte[] Body, double Time)>();
var trace = new RuntimeTraceRecorder();
using IDisposable subscription =
host.Runtime.Subscribe(trace);
operations.Sessions[^1].GameActionCapture =
body => sent.Add((
body,
host.Runtime.Clock.SimulationTimeSeconds));
RuntimeCommandResult intent = host.Commands.Movement.SetIntent(
host.Runtime.Generation,
new MovementInput(Forward: true, Run: true));
host.Tick(0.015d);
Assert.True(intent.Accepted);
Assert.Contains(
trace.Entries,
static entry =>
entry.Kind == RuntimeTraceKind.Movement);
Assert.Equal(
[
MoveToState.MoveToStateAction,
AutonomousPosition.AutonomousPositionAction,
],
sent.Select(static entry =>
ActionOpcode(entry.Body)).ToArray());
sent.Clear();
for (int index = 0; index < 70; index++)
host.Tick(0.015d);
(byte[] Body, double Time)[] positions = sent
.Where(static entry =>
ActionOpcode(entry.Body)
== AutonomousPosition.AutonomousPositionAction)
.ToArray();
Assert.InRange(positions.Length, 1, 2);
Assert.True(positions[^1].Time >= 1d);
if (positions.Length == 2)
{
Assert.True(
positions[1].Time - positions[0].Time >= 0.99d);
}
Assert.DoesNotContain(
sent,
entry => ActionOpcode(entry.Body)
== MoveToState.MoveToStateAction);
}
[Fact]
public void TeardownRetriesOnlyTheUnfinishedSuffix()
{
var operations = new FixtureSessionOperations();
var writer = new FailOnceTextWriter();
using var credential = new HeadlessCredentialSecret(
"fixture",
"password");
var host = new HeadlessSessionHost(
Descriptor(),
credential,
new HeadlessDiagnosticWriter(writer),
operations);
Assert.Equal(
RuntimeSessionStartStatus.Connected,
host.Start().Status);
writer.FailNextWrite = true;
Assert.Throws<IOException>(host.Dispose);
Assert.Equal(1, operations.DisposedSessionCount);
host.Dispose();
Assert.Equal(1, operations.DisposedSessionCount);
Assert.True(host.Runtime.CaptureOwnership().IsConverged);
}
[Fact]
public void WorldProjectionHydratesCanonicalMovementAndTeleportState()
{
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 = 0x50000002u;
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 collision = new FixtureCollisionNeighborhood();
var projection = new HeadlessSessionWorldProjection(
runtime,
collision);
projection.ProjectSpawn(record, isLocalPlayer: true);
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
runtime.MovementOwner.Controller);
controller.SetPosition(
new Vector3(48f, 49f, 50f),
0xA9B40001u);
projection.ProjectPosition(
record,
isLocalPlayer: true,
PositionTimestampDisposition.Apply);
Assert.Same(controller, runtime.MovementOwner.Controller);
Assert.Equal(record.LocalEntityId, controller.LocalEntityId);
Assert.Equal(new Vector3(48f, 49f, 50f), controller.Position);
Assert.Equal(
0xA9B40000u,
controller.CellId & 0xFFFF0000u);
Assert.True((controller.CellId & 0xFFFFu) < 0x0100u);
projection.BeginTeleport();
Assert.Equal(PlayerState.PortalSpace, controller.State);
RuntimeDestinationReadiness readiness =
projection.PrepareDestination(
revealGeneration: 7,
new RuntimeTeleportDestination(
player,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 1,
ForcePositionSequence: 0,
new Position(
0xA9B40001u,
new Vector3(96f, 97f, 50f),
Quaternion.Identity)));
Assert.True(readiness.IsCollisionReady);
Assert.False(readiness.IsUnhydratable);
Assert.Equal(PlayerState.InWorld, controller.State);
Assert.Equal(3, collision.CenterCount);
Assert.Equal(0xA9B40001u, collision.LastCell);
}
[Fact]
public void WorldProjectionIgnoresNormalEchoButBlipsForcePosition()
{
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 = 0x50000003u;
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 collision = new FixtureCollisionNeighborhood();
var projection = new HeadlessSessionWorldProjection(
runtime,
collision);
projection.ProjectSpawn(record, isLocalPlayer: true);
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
runtime.MovementOwner.Controller);
controller.SetPosition(
new Vector3(48f, 49f, 50f),
0xA9B40001u);
projection.ProjectPosition(
record,
isLocalPlayer: true,
PositionTimestampDisposition.Apply);
Assert.Equal(new Vector3(48f, 49f, 50f), controller.Position);
var force = new WorldSession.EntityPositionUpdate(
player,
record.Snapshot.Position!.Value with
{
PositionX = 72f,
PositionY = 73f,
},
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 0,
ForcePositionSequence: 1);
Assert.True(runtime.EntityObjects.TryApplyPosition(
force,
isLocalPlayer: true,
forcePositionRotation: Quaternion.Identity,
currentLocalVelocity: controller.BodyVelocity,
projectionRequiresTeleportHook: false,
acknowledgeProjection: null,
out PositionTimestampDisposition disposition,
out _,
out _));
Assert.Equal(
PositionTimestampDisposition.ForcePosition,
disposition);
projection.ProjectPosition(
record,
isLocalPlayer: true,
disposition);
Assert.Equal(new Vector3(72f, 73f, 50f), controller.Position);
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 ExecutorCompletedReceiptIsAcknowledgeOnlyRegardlessOfRecordValidity()
{
// F1: mirrors PlacementReceiptUsesExactIncarnationAndDiscardIsAckOnly's
// stale-token half - proves ExecutorCompleted is acknowledged
// unconditionally (never gated by the record-lookup/portal-shape
// checks Place/Withdraw depend on), so a genuinely stale/mismatched
// token can never wedge the FIFO behind it.
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(0x50000006u))
.Canonical!;
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
record,
record.CreateIntegrationVersion,
record.Snapshot,
replaceGeneration: false));
var sink = new HeadlessRuntimePlacementProjectionSink(runtime);
RuntimePlacementProjectionSnapshot completion = Placement(
runtime,
record,
RuntimePlacementProjectionKind.ExecutorCompleted,
Vector3.One,
Quaternion.Identity);
RuntimePlacementProjectionSnapshot stale = completion with
{
Token = completion.Token with
{
Entity = completion.Token.Entity with
{
Incarnation = unchecked((ushort)(
completion.Token.Entity.Incarnation + 1)),
},
SessionLifetimeVersion = ulong.MaxValue,
},
};
Assert.True(sink.TryApply(in completion));
Assert.True(sink.TryApply(in stale));
}
[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()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu;
CompleteCollisionGeneration(
physics,
landblockId,
afterAdmission: null,
(admission, prepared) =>
physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(landblockId, 10f)));
Assert.Throws<FixtureCollisionPublicationException>(() =>
CompleteCollisionGeneration(
physics,
landblockId,
_ => throw new FixtureCollisionPublicationException(),
(_, _) => throw new InvalidOperationException(
"Staging must not run after the injected admission fault.")));
Assert.Equal(10f, physics.Engine.SampleTerrainZ(1f, 1f));
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
Assert.Equal(1, ownership.LandblockCount);
Assert.Equal(0, ownership.CollisionAdmissionCount);
}
[Fact]
public void CollisionTransactionYieldsTheFirstNonterminalRuntimePoll()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu;
HeadlessCollisionGenerationTransaction transaction =
HeadlessCollisionGenerationTransaction.Begin(
physics,
landblockId,
afterAdmission: null,
(admission, prepared) =>
physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(landblockId, 10f)));
HeadlessCollisionGenerationAdvance advance;
do
{
advance = transaction.Advance();
Assert.True(advance.Progressed);
}
while (!advance.YieldToCaller);
Assert.False(advance.Completed);
Assert.False(advance.WaitingForProjectionAcknowledgement);
Assert.False(transaction.CompletionCommitted);
do
{
advance = transaction.Advance();
Assert.True(advance.Progressed);
}
while (!advance.Completed && !advance.YieldToCaller);
if (!advance.Completed)
advance = transaction.Advance();
Assert.True(advance.Completed);
Assert.True(transaction.EngineMutationCommitted);
Assert.True(transaction.CompletionCommitted);
Assert.Equal(0, physics.CaptureOwnership().CollisionAdmissionCount);
}
[Fact]
public void CollisionTransactionRetainsPostEngineCancellationUntilPlaceAck()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu;
CompleteCollisionGeneration(
physics,
landblockId,
afterAdmission: null,
(admission, prepared) =>
physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(landblockId, 10f)));
const uint guid = 0x70004201u;
const uint cell = 0xA9B40001u;
Vector3 position = new(10f, 10f, 0f);
RuntimeEntityRecord record = lifetime.RegisterEntity(
Spawn(guid)).Canonical!;
lifetime.Entities.SetFinalPhysicsState(record, PhysicsStateFlags.Gravity);
lifetime.Entities.SetFullCell(record, cell, landblockId);
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();
physics.AcknowledgeSpatialProjection(record, spatial: true);
RuntimePlacementProjectionToken seeded = SeedRuntimePlacement(
physics,
record,
cell,
position);
Assert.True(physics.SetPosition.AcknowledgeProjection(seeded));
HeadlessCollisionGenerationTransaction transaction =
HeadlessCollisionGenerationTransaction.Begin(
physics,
landblockId,
afterAdmission: null,
(admission, prepared) =>
physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(landblockId, 20f)));
HeadlessCollisionGenerationAdvance advance;
do
{
advance = transaction.Advance();
}
while (!advance.WaitingForProjectionAcknowledgement);
Assert.False(transaction.EngineMutationCommitted);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawal));
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
do
{
advance = transaction.Advance();
}
while (!advance.WaitingForProjectionAcknowledgement);
Assert.True(transaction.EngineMutationCommitted);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot placement));
Assert.False(transaction.TryCancel());
Assert.True(physics.SetPosition.AcknowledgeProjection(placement.Token));
Assert.True(transaction.TryCancel());
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
Assert.Equal(0, ownership.CollisionAdmissionCount);
}
[Fact]
public void CollisionTransactionCancelsStagingFaultWithoutWithdrawingActiveWorld()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu;
CompleteCollisionGeneration(
physics,
landblockId,
afterAdmission: null,
(admission, prepared) =>
physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(landblockId, 10f)));
Assert.Throws<FixtureCollisionPublicationException>(() =>
CompleteCollisionGeneration(
physics,
landblockId,
afterAdmission: null,
(admission, prepared) =>
{
physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(landblockId, 25f));
throw new FixtureCollisionPublicationException();
}));
Assert.Equal(10f, physics.Engine.SampleTerrainZ(1f, 1f));
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
Assert.Equal(1, ownership.LandblockCount);
Assert.Equal(0, ownership.CollisionAdmissionCount);
}
private static HeadlessSessionDescriptor Descriptor(
HeadlessCredentialProviderKind provider =
HeadlessCredentialProviderKind.Environment,
string credentialReference = "BOT_PASSWORD") => new()
{
Id = "bot",
Endpoint = new HeadlessEndpointDescriptor
{
Host = "127.0.0.1",
Port = 9000,
},
Account = "account",
Character = new HeadlessCharacterSelector
{
Name = "headless",
},
Policy = new HeadlessBotPolicyDescriptor
{
Id = "idle",
},
Credential = new HeadlessCredentialReference
{
Provider = provider,
Reference = credentialReference,
},
};
private static void HydrateGroundedPlayer(GameRuntime runtime)
{
const uint player = 0x50000002u;
PhysicsEngine engine = runtime.EntityObjects.Physics.Engine;
AddFlatLandblock(engine);
RuntimeEntityRecord record = runtime.EntityObjects
.RegisterEntity(Spawn(player))
.Canonical!;
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
record,
record.CreateIntegrationVersion,
record.Snapshot,
replaceGeneration: false));
var controller = new PlayerMovementController(engine);
controller.SetPosition(
new Vector3(96f, 97f, 50f),
0xA9B40001u,
new Vector3(96f, 97f, 50f));
runtime.MovementOwner.Controller = controller;
}
private static void CompleteCollisionGeneration(
RuntimePhysicsState physics,
uint landblockId,
Action<RuntimeCollisionAdmission>? afterAdmission,
Action<RuntimeCollisionAdmission,
PreparedLandblockCollisionGeneration> stage)
{
HeadlessCollisionGenerationTransaction transaction =
HeadlessCollisionGenerationTransaction.Begin(
physics,
landblockId,
afterAdmission,
stage);
while (true)
{
HeadlessCollisionGenerationAdvance advance = transaction.Advance();
if (advance.Completed)
return;
Assert.True(advance.Progressed);
Assert.False(advance.WaitingForProjectionAcknowledgement);
}
}
private static RuntimePlacementProjectionToken SeedRuntimePlacement(
RuntimePhysicsState physics,
RuntimeEntityRecord record,
uint cell,
Vector3 position)
{
Type coreMarker = typeof(PhysicsEngine);
Type requestType = coreMarker.Assembly.GetType(
"AcDream.Core.Physics.PhysicsSetPositionRequest",
throwOnError: true)!;
Type flagsType = coreMarker.Assembly.GetType(
"AcDream.Core.Physics.PhysicsSetPositionFlags",
throwOnError: true)!;
Type placementClassType = coreMarker.Assembly.GetType(
"AcDream.Core.Physics.PhysicsPlacementClass",
throwOnError: true)!;
object request = Activator.CreateInstance(
requestType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
args:
[
position,
Quaternion.Identity,
cell,
position,
ImmutableArray<FlatCollisionSphere>.Empty,
1f,
0.4f,
0.4f,
PhysicsStateFlags.None,
ObjectInfoState.None,
0u,
Enum.ToObject(placementClassType, 0),
Enum.ToObject(flagsType, 0x011u),
Vector3.Zero,
0f,
0f,
0u,
cell,
],
culture: null)!;
Type runtimeMarker = typeof(RuntimePhysicsState);
Type commandType = runtimeMarker.Assembly.GetType(
"AcDream.Runtime.Physics.RuntimeSetPositionCommand",
throwOnError: true)!;
Type kindType = runtimeMarker.Assembly.GetType(
"AcDream.Runtime.Physics.RuntimeSetPositionOperationKind",
throwOnError: true)!;
object command = Activator.CreateInstance(
commandType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
args:
[
request,
Enum.ToObject(kindType, 2),
10d,
0UL,
0f,
0f,
default(RuntimePortalPlacementAuthority),
],
culture: null)!;
MethodInfo apply = physics.SetPosition.GetType().GetMethod(
"Apply",
BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new MissingMethodException("Runtime SetPosition.Apply");
object outcome = apply.Invoke(
physics.SetPosition,
[record, record.PositionAuthorityVersion, command])!;
return (RuntimePlacementProjectionToken)(outcome.GetType().GetProperty(
"Projection",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
?.GetValue(outcome)
?? throw new MissingMemberException("Runtime placement projection"));
}
private static RuntimeLandblockCollisionAssets CollisionAssets(
uint landblockId,
float terrainHeight)
{
var heights = new byte[81];
var table = new float[256];
table[0] = terrainHeight;
return new RuntimeLandblockCollisionAssets(
landblockId,
new TerrainSurface(heights, table),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f,
0u);
}
private sealed class FixtureCollisionPublicationException : Exception;
private static void AddFlatLandblock(PhysicsEngine engine)
{
var heights = new byte[81];
Array.Fill(heights, (byte)50);
var heightTable = new float[256];
for (int index = 0; index < heightTable.Length; index++)
heightTable[index] = index;
engine.AddLandblock(
0xA9B4FFFFu,
new TerrainSurface(heights, heightTable),
[],
[],
worldOffsetX: 0f,
worldOffsetY: 0f);
}
private static WorldSession.EntitySpawn Spawn(uint guid)
{
var position = new CreateObject.ServerPosition(
0xA9B40001u,
96f,
97f,
50f,
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: 1);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: null,
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,
[],
[],
[],
null,
null,
"Headless",
null,
null,
null,
PhysicsState: physics.RawState,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
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)));
private sealed class FixtureSessionOperations : ILiveSessionOperations
{
public List<WorldSession> Sessions { get; } = [];
public int CreatedSessionCount { get; private set; }
public int DisposedSessionCount { get; private set; }
public string? LastUser { get; private set; }
public string? LastPassword { get; private set; }
public IPEndPoint ResolveEndpoint(string host, int port) =>
new(IPAddress.Loopback, port);
public WorldSession CreateSession(IPEndPoint endpoint)
{
CreatedSessionCount++;
var session = new WorldSession(endpoint);
Sessions.Add(session);
return session;
}
public void Connect(
WorldSession session,
string user,
string password)
{
LastUser = user;
LastPassword = password;
}
public CharacterList.Parsed GetCharacters(
WorldSession session) =>
new(
0u,
[
new CharacterList.Character(
0x50000001u,
"Other",
0u),
new CharacterList.Character(
0x50000002u,
"Headless",
0u),
],
[],
11,
"account",
true,
true);
public void EnterWorld(
WorldSession session,
int activeCharacterIndex)
{
}
public void Tick(WorldSession session)
{
}
public void DisposeSession(WorldSession session)
{
DisposedSessionCount++;
session.Dispose();
}
}
private sealed class FailOnceTextWriter : StringWriter
{
public bool FailNextWrite { get; set; }
public override void WriteLine(string? value)
{
if (FailNextWrite)
{
FailNextWrite = false;
throw new IOException("fixture write failure");
}
base.WriteLine(value);
}
}
private sealed class FixtureCollisionNeighborhood
: IHeadlessCollisionNeighborhood
{
public int CenterCount { get; private set; }
public uint LastCell { get; private set; }
public void CenterOn(uint fullCellId)
{
CenterCount++;
LastCell = fullCellId;
}
public bool IsReady(uint fullCellId) =>
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");
}
}
}
}