Campaign P remaining-physics-divergence, placement cutover slice C3c
(docs/plans/2026-08-02-placement-cutover.md). Both production hosts now
register every initial Create through the residence + continuation-
executor + first-entry-conductor machinery (C0-C3b):
- Graphical (route 1): RegisterEntityWithInitialResidence at Create; the
shared RuntimeFirstEntryDriveController pumps both conductors from the
placement-receipt flow; MaterializeProjection and RebucketLiveEntity
are presentation-only while a residence is ACTIVE (ExecutorCompleted is
the presentation-binding receipt); post-residence entities take the
full legacy path including the prepare_to_enter_world clock edges.
PlayerModeController attaches presentation to the Runtime-published
controller; its legacy resolve/step-heights/host-construction path is
deleted; presentation-only rollback (retail has no entry-flow rollback).
- Headless (route 8): OnSpawned registers with residence when a drive
exists; content-less sessions keep the pre-flip direct registration;
SynchronizeLocalPlayer/CreateController/ApplySetupStepHeights deleted;
prepared-collision read failure is a typed AwaitingCollisionSource
retry; far remotes outside the service window complete celless.
- RuntimeLocalPlayerMovementState.Controller setter sealed internal; all
controller mutation flows through the publication lifecycle.
Fix slices landed within this cutover, each dual-gated:
- F1: live movement-stat/server-physics application routed through the
Runtime ownership seam (post-logout ingest crash on the retired
controller eliminated; RuntimeMovementSkillProjection deleted).
- F2: login activation wedge - collision-admission prefix gate factored
out of the seal (reentrant-commit RejectedAuthority), rearm generation
identity corrected, PlayerModeAutoEntry requires the Runtime-published
controller (world reveal can no longer seal unmaterialized).
- F3: landblock-prefix 0-sentinel replaced by explicit absent-id guards;
map-corner landblocks (grid row/col 0) fully legal through admission,
park/rearm/retire, quiescence, and outdoor shadow seeds.
- F5: local-player first-entry ground contact seeded by the shared
SpawnPlacementSettler (moved App->Core) at FinalizeActivation - the
retail first-gravity-frame touch (enter_world 0x00516170 carries no
seed); the legacy unconditional force-seed is overwritten by a real
floor-found contact; airborne spawns stay airborne; outbound contact
bit verified end-to-end. Fixes the standing-cast 'You can't do that
while in the air!' rejections.
- R1 (dual-review round): login constraint leash armed at the committed
placement (HandleReceivedPosition 0x00453FD0 analog); register rows
AD-61 (settle-timing compression now covering the local player) and
AD-42 (repointed off the deleted resolve split) in this commit;
residence-conversion owner API; wire-landblock guards; drive-pending
ledger in IsConverged; route attach/detach latch; executor-drain drift
model documented + source-pinned.
Gates: Runtime 1,003, App 4,039/3 skips, Headless 79, complete solution
10,816/0 failed/4 skips (Release, -m:1); connected lifecycle/reconnect
gate PASS (logs/connected-world-gate-20260802-175401; graceful exits,
world-visible, zero airborne rejections). The nine-stop soak remains red
for the pre-existing 6b28ff99 whole-world collision-clone throughput
regression (attributed with evidence; scheduled as its own slice before
C5). Dual Opus reviews (retail-conformance + adversarial): delta PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1595 lines
58 KiB
C#
1595 lines
58 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;
|
|
// C3c: conductor-driven flow — a live generation admits the initial
|
|
// residence, the flat landblock's collision generation commits, and
|
|
// the world projection's pump drives the local first-entry conductor
|
|
// to completion (the deleted SynchronizeLocalPlayer hand-copy's
|
|
// replacement).
|
|
Assert.Equal(
|
|
RuntimeSessionStartStatus.Connected,
|
|
host.Start().Status);
|
|
const uint player = 0x50000002u;
|
|
runtime.PlayerIdentity.ServerGuid = player;
|
|
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
|
0xA9B40000u, 1UL);
|
|
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
|
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
|
0xA9B40000u, 1UL, ready: true);
|
|
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
|
|
CreateFirstEntryDrive(runtime);
|
|
RuntimeEntityRecord record = runtime.EntityObjects
|
|
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
|
record,
|
|
record.CreateIntegrationVersion,
|
|
record.Snapshot,
|
|
replaceGeneration: false));
|
|
var collision = new FixtureCollisionNeighborhood();
|
|
var projection = new HeadlessSessionWorldProjection(
|
|
runtime,
|
|
collision,
|
|
firstEntry);
|
|
|
|
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;
|
|
// C3c: conductor-driven flow — a live generation admits the initial
|
|
// residence, the flat landblock's collision generation commits, and
|
|
// the world projection's pump drives the local first-entry conductor
|
|
// to completion (the deleted SynchronizeLocalPlayer hand-copy's
|
|
// replacement).
|
|
Assert.Equal(
|
|
RuntimeSessionStartStatus.Connected,
|
|
host.Start().Status);
|
|
const uint player = 0x50000003u;
|
|
runtime.PlayerIdentity.ServerGuid = player;
|
|
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
|
0xA9B40000u, 1UL);
|
|
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
|
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
|
0xA9B40000u, 1UL, ready: true);
|
|
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
|
|
CreateFirstEntryDrive(runtime);
|
|
RuntimeEntityRecord record = runtime.EntityObjects
|
|
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
|
record,
|
|
record.CreateIntegrationVersion,
|
|
record.Snapshot,
|
|
replaceGeneration: false));
|
|
var collision = new FixtureCollisionNeighborhood();
|
|
var projection = new HeadlessSessionWorldProjection(
|
|
runtime,
|
|
collision,
|
|
firstEntry);
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C3c-R1 review F7: a remote Create whose landblock lies outside the
|
|
/// bounded collision neighborhood's service window can never see its
|
|
/// deferred placement's collision-generation wake — left alone it would
|
|
/// pin its residence (and the drive's pending entry) for the whole
|
|
/// session. The host converts it to the celless completion route before
|
|
/// the pump: the residence completes with FullCell 0, the accepted wire
|
|
/// frame stays on the canonical snapshot (the exact pre-flip
|
|
/// accepted-frame behavior for far remotes), and every ledger converges.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FarRemoteCreateCompletesCelllessWithoutPinningItsResidence()
|
|
{
|
|
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;
|
|
Assert.Equal(
|
|
RuntimeSessionStartStatus.Connected,
|
|
host.Start().Status);
|
|
const uint player = 0x5000000Bu;
|
|
runtime.PlayerIdentity.ServerGuid = player;
|
|
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
|
0xA9B40000u, 1UL);
|
|
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
|
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
|
0xA9B40000u, 1UL, ready: true);
|
|
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
|
|
CreateFirstEntryDrive(runtime);
|
|
var collision = new FixtureCollisionNeighborhood();
|
|
var projection = new HeadlessSessionWorldProjection(
|
|
runtime,
|
|
collision,
|
|
firstEntry);
|
|
RuntimeEntityRecord playerRecord = runtime.EntityObjects
|
|
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
|
playerRecord,
|
|
playerRecord.CreateIntegrationVersion,
|
|
playerRecord.Snapshot,
|
|
replaceGeneration: false));
|
|
projection.ProjectSpawn(playerRecord, isLocalPlayer: true);
|
|
|
|
const uint farRemote = 0x70000010u;
|
|
const uint farCell = 0x00010001u;
|
|
Assert.False(collision.IsWithinServiceWindow(farCell));
|
|
RuntimeEntityRecord remote = runtime.EntityObjects
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(farRemote, farCell),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
|
remote,
|
|
remote.CreateIntegrationVersion,
|
|
remote.Snapshot,
|
|
replaceGeneration: false));
|
|
|
|
projection.ProjectSpawn(remote, isLocalPlayer: false);
|
|
|
|
// Celless completion: no pinned residence, FullCell stays 0, the
|
|
// accepted wire frame survives on the canonical snapshot.
|
|
Assert.False(runtime.EntityObjects.TryGetInitialCreateResidence(
|
|
remote,
|
|
out _));
|
|
Assert.Equal(0u, remote.FullCellId);
|
|
Assert.Equal(
|
|
farCell,
|
|
remote.Snapshot.Position!.Value.LandblockId);
|
|
RuntimeEntityObjectOwnershipSnapshot ownership =
|
|
runtime.EntityObjects.CaptureOwnership();
|
|
Assert.Equal(0, ownership.InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, ownership.FirstEntryDrivePendingCount);
|
|
Assert.Equal(0, firstEntry.PendingCount);
|
|
|
|
// Draining the placement FIFO the way the host subscription would
|
|
// converges the completion-receipt ledger too.
|
|
while (runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot head))
|
|
{
|
|
if (!runtime.EntityObjects.Physics.SetPosition
|
|
.AcknowledgeProjection(head.Token))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
Assert.Equal(
|
|
0,
|
|
runtime.EntityObjects.CaptureOwnership()
|
|
.PendingCompletionReceiptCount);
|
|
}
|
|
|
|
[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;
|
|
// C3c: conductor-driven flow — a live generation admits the initial
|
|
// residence, the flat landblock's collision generation commits, and
|
|
// the world projection's pump drives the local first-entry conductor
|
|
// to completion (the deleted SynchronizeLocalPlayer hand-copy's
|
|
// replacement).
|
|
Assert.Equal(
|
|
RuntimeSessionStartStatus.Connected,
|
|
host.Start().Status);
|
|
const uint player = 0x50000004u;
|
|
runtime.PlayerIdentity.ServerGuid = player;
|
|
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
|
0xA9B40000u, 1UL);
|
|
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
|
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
|
0xA9B40000u, 1UL, ready: true);
|
|
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
|
|
CreateFirstEntryDrive(runtime);
|
|
RuntimeEntityRecord record = runtime.EntityObjects
|
|
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
|
record,
|
|
record.CreateIntegrationVersion,
|
|
record.Snapshot,
|
|
replaceGeneration: false));
|
|
var directProjection = new HeadlessSessionWorldProjection(
|
|
runtime,
|
|
new FixtureCollisionNeighborhood(),
|
|
firstEntry);
|
|
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;
|
|
|
|
[Fact]
|
|
public void MissingPreparedCollisionYieldsTypedRetryAndCompletesWhenAvailable()
|
|
{
|
|
// C3c contract: the prepared-collision read failure is the
|
|
// conductor's typed AwaitingCollisionSource retry — no
|
|
// InvalidDataException (or any exception) escapes the host wiring,
|
|
// the entity stays a tracked, re-drivable first entry, and the same
|
|
// sequence completes once the source can serve the Setup.
|
|
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;
|
|
Assert.Equal(
|
|
RuntimeSessionStartStatus.Connected,
|
|
host.Start().Status);
|
|
const uint player = 0x50000021u;
|
|
runtime.PlayerIdentity.ServerGuid = player;
|
|
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
|
0xA9B40000u, 1UL);
|
|
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
|
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
|
0xA9B40000u, 1UL, ready: true);
|
|
var source = new FlakySetupCollisionSource();
|
|
var firstEntry = new AcDream.Runtime.Session
|
|
.RuntimeFirstEntryDriveController(
|
|
runtime.EntityObjects,
|
|
runtime.Clock,
|
|
source,
|
|
() => PlayerMovementConstructionOptions.From(
|
|
runtime.CharacterOwner.MovementSkills.Snapshot),
|
|
static _ => new RuntimeLocalPlayerPhysicsActivationPreparation(
|
|
Radius: 0.48f,
|
|
Height: 1.835f,
|
|
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
|
|
RuntimeEntityRecord record = runtime.EntityObjects
|
|
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
|
record,
|
|
record.CreateIntegrationVersion,
|
|
record.Snapshot,
|
|
replaceGeneration: false));
|
|
var collision = new FixtureCollisionNeighborhood();
|
|
var projection = new HeadlessSessionWorldProjection(
|
|
runtime,
|
|
collision,
|
|
firstEntry);
|
|
|
|
projection.ProjectSpawn(record, isLocalPlayer: true);
|
|
|
|
Assert.True(source.SetupReadAttempts >= 1);
|
|
Assert.Null(runtime.MovementOwner.Controller);
|
|
Assert.Equal(1, firstEntry.PendingCount);
|
|
Assert.Equal(0u, record.FullCellId);
|
|
|
|
source.Available = true;
|
|
// The session tick's retry pump.
|
|
firstEntry.DriveAll();
|
|
|
|
Assert.IsType<PlayerMovementController>(
|
|
runtime.MovementOwner.Controller);
|
|
Assert.Equal(0, firstEntry.PendingCount);
|
|
Assert.Equal(0xA9B40000u, record.FullCellId & 0xFFFF0000u);
|
|
Assert.NotEqual(0u, record.FullCellId);
|
|
}
|
|
|
|
private sealed class FlakySetupCollisionSource
|
|
: AcDream.Content.IPreparedCollisionSource
|
|
{
|
|
internal bool Available { get; set; }
|
|
internal int SetupReadAttempts { get; private set; }
|
|
|
|
public AcDream.Content.PreparedAssetPresence ProbeCollision(
|
|
AcDream.Content.Pak.PakAssetType type,
|
|
uint sourceFileId) =>
|
|
AcDream.Content.PreparedAssetPresence.Available;
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<FlatSetupCollision>
|
|
ReadSetupCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
SetupReadAttempts++;
|
|
if (!Available)
|
|
{
|
|
return AcDream.Content.PreparedCollisionReadResult<
|
|
FlatSetupCollision>.Missing;
|
|
}
|
|
return AcDream.Content.PreparedCollisionReadResult<
|
|
FlatSetupCollision>.Loaded(new FlatSetupCollision(
|
|
System.Collections.Immutable.ImmutableArray<
|
|
FlatCollisionCylinder>.Empty,
|
|
[new FlatCollisionSphere(Vector3.Zero, 0.48f)],
|
|
height: 0f,
|
|
radius: 0f,
|
|
stepUpHeight: 0.4f,
|
|
stepDownHeight: 0.4f));
|
|
}
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<
|
|
FlatGfxObjCollisionAsset> ReadGfxObjCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<
|
|
FlatCellStructureCollisionAsset> ReadCellStructureCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<FlatEnvCellTopology>
|
|
ReadEnvCellTopology(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public AcDream.Content.PreparedCollisionSourceStats CollisionStats =>
|
|
default;
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private static AcDream.Runtime.Session.RuntimeFirstEntryDriveController
|
|
CreateFirstEntryDrive(GameRuntime runtime) => new(
|
|
runtime.EntityObjects,
|
|
runtime.Clock,
|
|
new LoadedSetupCollisionSource(),
|
|
() => PlayerMovementConstructionOptions.From(
|
|
runtime.CharacterOwner.MovementSkills.Snapshot),
|
|
static _ => new RuntimeLocalPlayerPhysicsActivationPreparation(
|
|
Radius: 0.48f,
|
|
Height: 1.835f,
|
|
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
|
|
|
|
private sealed class LoadedSetupCollisionSource
|
|
: AcDream.Content.IPreparedCollisionSource
|
|
{
|
|
public AcDream.Content.PreparedAssetPresence ProbeCollision(
|
|
AcDream.Content.Pak.PakAssetType type,
|
|
uint sourceFileId) =>
|
|
AcDream.Content.PreparedAssetPresence.Available;
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<FlatSetupCollision>
|
|
ReadSetupCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
AcDream.Content.PreparedCollisionReadResult<FlatSetupCollision>
|
|
.Loaded(new FlatSetupCollision(
|
|
System.Collections.Immutable.ImmutableArray<
|
|
FlatCollisionCylinder>.Empty,
|
|
[new FlatCollisionSphere(Vector3.Zero, 0.48f)],
|
|
height: 0f,
|
|
radius: 0f,
|
|
stepUpHeight: 0.4f,
|
|
stepDownHeight: 0.4f));
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<
|
|
FlatGfxObjCollisionAsset> ReadGfxObjCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<
|
|
FlatCellStructureCollisionAsset> ReadCellStructureCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<FlatEnvCellTopology>
|
|
ReadEnvCellTopology(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public AcDream.Content.PreparedCollisionSourceStats CollisionStats =>
|
|
default;
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
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,
|
|
uint cellId = 0xA9B40001u)
|
|
{
|
|
var position = new CreateObject.ServerPosition(
|
|
cellId,
|
|
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;
|
|
|
|
// C3c-R1 review F7: the fixture window mirrors production's 3x3
|
|
// membership around the last requested center; no center yet means
|
|
// "within" (never convert before the first CenterOn).
|
|
public bool IsWithinServiceWindow(uint fullCellId)
|
|
{
|
|
if (LastCell == 0u)
|
|
return true;
|
|
int dx = Math.Abs(
|
|
(int)((fullCellId >> 24) & 0xFFu)
|
|
- (int)((LastCell >> 24) & 0xFFu));
|
|
int dy = Math.Abs(
|
|
(int)((fullCellId >> 16) & 0xFFu)
|
|
- (int)((LastCell >> 16) & 0xFFu));
|
|
return dx <= 1 && dy <= 1;
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|