TWO work products share this commit (a staged-index collision between the
coordinator's docs commit and the OP7 fixer's staged files — content
verified complete and coherent; only this message was wrong before the
amend):
1. OP7 review fixes (all nine findings from
docs/research/2026-08-11-op7-review.md):
- M1: HeadlessSessionDescriptor is a record; WithAccount uses 'with' non-destructive record copy,
so a future property cannot be silently dropped; direct-CLI
regression test proves CharacterOptions survives --user/--password.
- M2 root fix: LiveSessionEventRouter skips BOTH Replace and the
options notification on a trailer-truncated PlayerDescription — a
truncated re-seed can no longer install zeroed words under an armed
latch for OP7's automation to flush into 0x01A1.
- SF1: schema keys validate as ordinal strings against the allowed
names (numeric / comma-combined aliases rejected). SF2: both-true
fellowship exclusion rejected at load, naming both keys. SF3: the
onLoginCompleteSent observer moved after transit.EndTeleport().
SF4: production-hook coverage for all three LoginComplete sites.
SF5: test-script OP7 wire expectation corrected (batched ids ride
only the 0x01A1).
2. docs/research/2026-08-11-op3-rereview.md — OP3 re-review verdict
REOPEN (narrow): M1 byte-decode independently re-verified (6a 07 at
all six sites); residuals R1 (gate script promises a timestamp prefix
acdream doesn't render), R2 (null-controller player-mode still
refuses), R3 (dormancy pin lacks stimulus) — coordinator fixes follow.
Full Release suite at this tree: 12,956 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2846 lines
115 KiB
C#
2846 lines
115 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 async Task DirectCredentialsPreserveDeclaredCharacterOptions()
|
||
{
|
||
// MF-1 (Campaign OP OP7 review fix, 2026-08-11): the K3 direct-CLI
|
||
// launch mode (--user/--password) rebuilds the configured session
|
||
// descriptor via HeadlessProcessHost.WithAccount before constructing
|
||
// HeadlessSessionHost. A hand-copied clone that forgets a property
|
||
// silently drops that feature for this launch mode with no error —
|
||
// exactly what happened to CharacterOptions. Assert the declared
|
||
// block survives the direct-credential path by checking the
|
||
// constructed session's seeder actually saw it.
|
||
var configuration = new HeadlessConfiguration
|
||
{
|
||
Version = 1,
|
||
Sessions =
|
||
[
|
||
Descriptor(characterOptions: new Dictionary<string, bool>
|
||
{
|
||
["AutoRepeatAttack"] = true,
|
||
}),
|
||
],
|
||
};
|
||
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.NotNull(host.Session.OptionsSeeder);
|
||
Assert.True(host.Session.OptionsSeeder!.HasDeclaredOptions);
|
||
}
|
||
|
||
[Fact]
|
||
public void DirectFirstEntryCompletion_InvokesOnLoginCompleteSentHookThroughProductionWiring()
|
||
{
|
||
// SF-4 (Campaign OP OP7 review fix, 2026-08-11): the THIRD
|
||
// production LoginComplete->seeder hook lives entirely inside
|
||
// HeadlessSessionHost.CreateEventRoute's own closure (the direct,
|
||
// non-portal first-entry completion callback wired to
|
||
// RuntimeFirstEntryDriveController via HeadlessSessionEventRoute's
|
||
// localPlayerCompleted parameter) — driving it end-to-end would
|
||
// need a real DAT-backed collision fixture this test project does
|
||
// not have. Reflection reaches the SAME closure instance
|
||
// HeadlessSessionHost actually constructed during Start() (not a
|
||
// hand-rolled reconstruction of it) and invokes it exactly as
|
||
// RuntimeFirstEntryDriveController would on residence completion,
|
||
// then proves the seeder reacted through the real wire — the same
|
||
// "declared option actually sends" proof
|
||
// HeadlessCharacterOptionsSeederWiringTests uses for the other two
|
||
// sites.
|
||
var operations = new FixtureSessionOperations();
|
||
using var credential = new HeadlessCredentialSecret(
|
||
"fixture",
|
||
"password");
|
||
using var host = new HeadlessSessionHost(
|
||
Descriptor(characterOptions: new Dictionary<string, bool>
|
||
{
|
||
["IgnoreAllegianceRequests"] = true,
|
||
}),
|
||
credential,
|
||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||
operations);
|
||
Assert.Equal(
|
||
RuntimeSessionStartStatus.Connected,
|
||
host.Start().Status);
|
||
Assert.NotNull(host.OptionsSeeder);
|
||
|
||
WorldSession session = operations.Sessions[^1];
|
||
var sent = new List<byte[]>();
|
||
session.GameActionCapture = body => sent.Add(body);
|
||
|
||
object eventRoute = typeof(HeadlessSessionHost)
|
||
.GetField(
|
||
"_eventRoute",
|
||
BindingFlags.NonPublic | BindingFlags.Instance)!
|
||
.GetValue(host)
|
||
?? throw new InvalidOperationException(
|
||
"HeadlessSessionHost constructed no event route.");
|
||
var localPlayerCompleted =
|
||
(Action<RuntimeEntityRecord>?)typeof(HeadlessSessionEventRoute)
|
||
.GetField(
|
||
"_localPlayerCompleted",
|
||
BindingFlags.NonPublic | BindingFlags.Instance)!
|
||
.GetValue(eventRoute);
|
||
Assert.NotNull(localPlayerCompleted);
|
||
|
||
const uint playerGuid = 0x50000009u;
|
||
host.Runtime.PlayerIdentity.ServerGuid = playerGuid;
|
||
RuntimeEntityRecord record = host.Runtime.EntityObjects
|
||
.RegisterEntity(Spawn(playerGuid))
|
||
.Canonical!;
|
||
|
||
// Invoke the PRODUCTION closure directly — proves
|
||
// HeadlessSessionHost really wires _optionsSeeder?.NoteLoginCompleteSent()
|
||
// into this callback, not merely that some test double does.
|
||
localPlayerCompleted!(record);
|
||
|
||
Assert.Contains(
|
||
sent,
|
||
body => body.SequenceEqual(GameActionLoginComplete.Build()));
|
||
|
||
// No PlayerDescription has landed yet — HasServerSeed is still
|
||
// false, so nothing beyond LoginComplete can have sent.
|
||
Assert.DoesNotContain(
|
||
sent,
|
||
body => ActionOpcode(body)
|
||
== SocialActions.SetSingleCharacterOptionOpcode);
|
||
|
||
session.GameEvents.Dispatch(
|
||
GameEventEnvelope.TryParse(
|
||
WrapPlayerDescriptionEnvelope(options1: 0u, options2: 0u))!
|
||
.Value);
|
||
|
||
Assert.Contains(
|
||
sent,
|
||
body => ActionOpcode(body)
|
||
== SocialActions.SetSingleCharacterOptionOpcode);
|
||
}
|
||
|
||
[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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// B5(a) review fix: <see cref="HeadlessSessionEventRouteRetryPendingTests"/>
|
||
/// proved the underlying re-offer MECHANISM works, but hand-constructed
|
||
/// <see cref="HeadlessSessionEventRoute"/> directly and called
|
||
/// <c>route.RetryPending()</c> itself — it never touches
|
||
/// <see cref="HeadlessSessionHost.Tick"/>'s own
|
||
/// <c>_eventRoute?.RetryPending()</c> call. This test drives <c>Tick</c>
|
||
/// itself (via the <c>placementSinkOverride</c> test seam added for this
|
||
/// fix, mirroring the existing <c>policyOverride</c> parameter) so a
|
||
/// regression that deletes or reorders that exact line would fail HERE,
|
||
/// not just in the lower-level subscription test.
|
||
/// </summary>
|
||
[Fact]
|
||
public void TickRetriesAPreviouslyDeclinedPlacementThroughTheRealEventRoute()
|
||
{
|
||
const uint remote = 0x70004301u;
|
||
const uint landblock = 0xA9B40000u;
|
||
const uint cell = landblock | 0x0001u;
|
||
const float height = 6f;
|
||
|
||
var operations = new FixtureSessionOperations();
|
||
using var credential = new HeadlessCredentialSecret(
|
||
"fixture",
|
||
"password");
|
||
var sink = new DecliningThenAcceptingPlacementSink();
|
||
using var host = new HeadlessSessionHost(
|
||
Descriptor(),
|
||
credential,
|
||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||
operations,
|
||
placementSinkOverride: sink);
|
||
GameRuntime runtime = host.Runtime;
|
||
Assert.Equal(
|
||
RuntimeSessionStartStatus.Connected,
|
||
host.Start().Status);
|
||
|
||
runtime.EntityObjects.Physics.ObserveLocalWorldFrame(
|
||
cell, teleportAdvanced: false);
|
||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||
landblock, 1UL);
|
||
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
||
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||
landblock, 1UL, ready: true);
|
||
|
||
RuntimeEntityRecord record = runtime.EntityObjects
|
||
.RegisterEntity(Spawn(remote, cell))
|
||
.Canonical!;
|
||
runtime.EntityObjects.Entities.SetFinalPhysicsState(
|
||
record, PhysicsStateFlags.Gravity);
|
||
runtime.EntityObjects.Entities.SetFullCell(
|
||
record, cell, landblock);
|
||
var body = new PhysicsBody
|
||
{
|
||
Position = new Vector3(10f, 10f, height),
|
||
Orientation = Quaternion.Identity,
|
||
LastUpdateTime = 1d,
|
||
State = PhysicsStateFlags.Gravity,
|
||
TransientState = TransientStateFlags.Active,
|
||
};
|
||
body.SnapToCell(cell, body.Position, body.Position);
|
||
runtime.EntityObjects.Entities.SetPhysicsBody(record, body);
|
||
record.ObjectClock.Activate();
|
||
runtime.EntityObjects.Physics.AcknowledgeSpatialProjection(
|
||
record, spatial: true);
|
||
|
||
RuntimeEntityPlacementToken token = runtime.EntityObjects.Physics
|
||
.SetPosition.TryBeginExclusiveAuthoredPlacement(
|
||
record,
|
||
record.PositionAuthorityVersion,
|
||
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
||
Assert.True(token.IsValid);
|
||
RuntimeSetPositionMoverPreparationStatus status = runtime.EntityObjects
|
||
.Physics.SetPosition.TryPrepareAndSubmitAuthoredPlacement(
|
||
record,
|
||
token,
|
||
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
||
PhysicsSetPositionFlags.Teleport | PhysicsSetPositionFlags.Slide,
|
||
new LoadedSetupCollisionSource(),
|
||
gameTime: runtime.Clock.SimulationTimeSeconds,
|
||
out RuntimeSetPositionOutcome outcome,
|
||
resolveWorldOffsetFromRuntimeFrame: true);
|
||
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared, status);
|
||
Assert.Equal(
|
||
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
||
outcome.Status);
|
||
|
||
// The production HeadlessSessionEventRoute's subscription attached
|
||
// during host.Start() already observed this Place synchronously —
|
||
// the fake sink is still declining, so it must remain unacknowledged.
|
||
Assert.Equal(1, sink.CallCount);
|
||
Assert.True(
|
||
runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
|
||
out _));
|
||
|
||
// The sink starts accepting (mirrors a landblock finishing streaming
|
||
// in) — driving ONE real host tick is what must re-offer the head,
|
||
// through Tick's own wiring, not a hand-built route.
|
||
sink.Accept = true;
|
||
host.Tick(0.015d);
|
||
|
||
Assert.Equal(2, sink.CallCount);
|
||
Assert.False(
|
||
runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
|
||
out _));
|
||
}
|
||
|
||
private sealed class DecliningThenAcceptingPlacementSink
|
||
: IRuntimePlacementProjectionSink
|
||
{
|
||
internal int CallCount { get; private set; }
|
||
internal bool Accept { get; set; }
|
||
|
||
public bool TryApply(in RuntimePlacementProjectionSnapshot projection)
|
||
{
|
||
CallCount++;
|
||
return Accept;
|
||
}
|
||
}
|
||
|
||
[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();
|
||
// R5/A7 review fix (2026-08-05): the drive controller is now wired
|
||
// (was omitted in the first pass, leaving the canonical portal arm
|
||
// a no-op by construction here and dual-host parity with zero
|
||
// coverage). Mirrors production composition
|
||
// (HeadlessSessionHost.cs's own construction order): the SAME
|
||
// RuntimeAcceptedPositionDriveController drives both hosts through
|
||
// the identical TryExecuteAcceptedPortalArrival entry point.
|
||
RuntimeAcceptedPositionDriveController acceptedPositionDrive =
|
||
CreateAcceptedPositionDrive(runtime);
|
||
var projection = new HeadlessSessionWorldProjection(
|
||
runtime,
|
||
collision,
|
||
firstEntry,
|
||
acceptedPositionDrive);
|
||
|
||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||
PlayerMovementController controller =
|
||
Assert.IsType<PlayerMovementController>(
|
||
runtime.MovementOwner.Controller);
|
||
controller.SeedPlacementForTest(
|
||
new Vector3(48f, 49f, 50f),
|
||
0xA9B40001u, new Vector3(48f, 49f, 50f));
|
||
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);
|
||
|
||
// R5/A7: the destination cell 0xA9B40001 is in the SAME landblock
|
||
// (0xA9B40000) whose collision generation the test already
|
||
// committed above, so the canonical portal arm resolves
|
||
// Committed synchronously - no DeferredCell park needed to exercise
|
||
// the real headless placement path. A1's headless retry loop
|
||
// (RuntimeLiveEntitySessionController.PumpPortalCompletion) is
|
||
// covered separately by
|
||
// HeadlessPortalDeferredCellCommitsOnPumpAfterCollisionGenerationWake
|
||
// below.
|
||
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)),
|
||
// R5/A7: a real, valid token - `default` was fine for the
|
||
// old no-op arm but RuntimePortalPlacementAuthority.IsValid
|
||
// now genuinely gates TryExecuteAcceptedPortalArrival on it.
|
||
new RuntimeWorldHostProjectionToken(7, 0xA9B40001u));
|
||
|
||
Assert.True(readiness.IsCollisionReady);
|
||
Assert.False(readiness.IsUnhydratable);
|
||
Assert.Equal(PlayerState.InWorld, controller.State);
|
||
Assert.Equal(2, collision.CenterCount);
|
||
Assert.Equal(0xA9B40001u, collision.LastCell);
|
||
// A4/dual-host parity: the canonical placement actually committed -
|
||
// the body moved to the destination Position, not just the
|
||
// collision-neighborhood bookkeeping that CenterCount/LastCell
|
||
// alone would have proven even with the earlier no-op arm. Z
|
||
// settles 0.005 above the wire value (the foot sphere's bottom
|
||
// sits at origin + 0.475 - 0.48, LoadedSetupCollisionSource's own
|
||
// doc comment, ISSUES.md #285) - X/Y are exact, Z is asserted
|
||
// within that settle tolerance.
|
||
Assert.Equal(96f, controller.Position.X);
|
||
Assert.Equal(97f, controller.Position.Y);
|
||
Assert.Equal(50f, controller.Position.Z, 0.01f);
|
||
// The resolved outdoor sub-cell index is derived from X/Y within
|
||
// the landblock (not the wire placeholder 0xA9B40001), same as the
|
||
// FIRST ProjectPosition assertion above (":406-409") only checks
|
||
// landblock+indoor-vs-outdoor, not the exact sub-cell.
|
||
Assert.Equal(0xA9B40000u, controller.CellId & 0xFFFF0000u);
|
||
Assert.True((controller.CellId & 0xFFFFu) < 0x0100u);
|
||
}
|
||
|
||
/// <summary>
|
||
/// A1/R5/A7 review fix (2026-08-05): headless has no per-frame anim
|
||
/// sequencer the way the graphical host does, so its OWN equivalent of
|
||
/// A1's "hold until committed" mechanism is
|
||
/// <c>HeadlessSessionWorldProjection.PrepareDestination</c>'s
|
||
/// <c>_awaitingPortalWake</c> polling. This proves it end to end: a
|
||
/// destination in a landblock whose collision generation is NOT yet
|
||
/// committed parks (<c>IsCollisionReady: false</c>, body unmoved, no
|
||
/// throw — DeferredCell is a normal headless outcome per
|
||
/// <c>PrepareDestination</c>'s own doc), and once the destination
|
||
/// landblock's collision generation commits, the SAME park resolves on
|
||
/// a later attempt WITHOUT a second concurrent Begin (Runtime's own
|
||
/// Begin would refuse that with Contention if this class re-attempted
|
||
/// blindly instead of polling <c>PendingCount</c>).
|
||
/// </summary>
|
||
[Fact]
|
||
public void HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake()
|
||
{
|
||
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 = 0x50000009u;
|
||
const uint destinationLandblock = 0xAAB40000u;
|
||
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();
|
||
RuntimeAcceptedPositionDriveController acceptedPositionDrive =
|
||
CreateAcceptedPositionDrive(runtime);
|
||
var projection = new HeadlessSessionWorldProjection(
|
||
runtime,
|
||
collision,
|
||
firstEntry,
|
||
acceptedPositionDrive);
|
||
|
||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||
PlayerMovementController controller =
|
||
Assert.IsType<PlayerMovementController>(
|
||
runtime.MovementOwner.Controller);
|
||
controller.SeedPlacementForTest(
|
||
new Vector3(48f, 49f, 50f),
|
||
0xA9B40001u, new Vector3(48f, 49f, 50f));
|
||
projection.ProjectPosition(
|
||
record,
|
||
isLocalPlayer: true,
|
||
PositionTimestampDisposition.Apply);
|
||
projection.BeginTeleport();
|
||
|
||
var destination = new RuntimeTeleportDestination(
|
||
player,
|
||
InstanceSequence: 1,
|
||
PositionSequence: 2,
|
||
TeleportSequence: 1,
|
||
ForcePositionSequence: 0,
|
||
new Position(
|
||
destinationLandblock | 0x0001u,
|
||
new Vector3(10f, 10f, 50f),
|
||
Quaternion.Identity));
|
||
var projectionToken = new RuntimeWorldHostProjectionToken(
|
||
7, destinationLandblock | 0x0001u);
|
||
|
||
// Begin's own portal-vs-latest-cell gate (D-T5) requires the
|
||
// destination's landblock to match the record's LATEST MERGED
|
||
// Position, not just the transit's retained destination - mirror
|
||
// what the real inbound Position handler already does before
|
||
// TryCompletePortal ever runs (LiveEntityNetworkUpdateController's
|
||
// App-side equivalent).
|
||
Assert.True(runtime.EntityObjects.TryApplyPosition(
|
||
new WorldSession.EntityPositionUpdate(
|
||
player,
|
||
new CreateObject.ServerPosition(
|
||
destination.Position.ObjCellId,
|
||
destination.Position.Frame.Origin.X,
|
||
destination.Position.Frame.Origin.Y,
|
||
destination.Position.Frame.Origin.Z,
|
||
destination.Position.Frame.Orientation.W,
|
||
destination.Position.Frame.Orientation.X,
|
||
destination.Position.Frame.Orientation.Y,
|
||
destination.Position.Frame.Orientation.Z),
|
||
Velocity: null,
|
||
PlacementId: null,
|
||
IsGrounded: true,
|
||
InstanceSequence: 1,
|
||
PositionSequence: 3,
|
||
TeleportSequence: destination.TeleportSequence,
|
||
ForcePositionSequence: 0),
|
||
isLocalPlayer: true,
|
||
forcePositionRotation: Quaternion.Identity,
|
||
currentLocalVelocity: Vector3.Zero,
|
||
acknowledgeProjection: null,
|
||
out _,
|
||
out _,
|
||
out _));
|
||
|
||
// First attempt: destinationLandblock's collision generation was
|
||
// never begun/committed, so the canonical arm parks DeferredCell.
|
||
// Must NOT throw (a park is normal, not an error). The dormant
|
||
// stage (RuntimeSetPositionState's SubmitPreparedPlacementCore
|
||
// deferred-commit path) already stages the body's Position/CellId
|
||
// at the destination while it waits (StageDormantCellFrame,
|
||
// body.InWorld=false) - the reader-visible Position moving early is
|
||
// that mechanism, not evidence the placement committed; only
|
||
// PlayerState/IsCollisionReady distinguish "staged" from
|
||
// "committed" here.
|
||
RuntimeDestinationReadiness parked = projection.PrepareDestination(
|
||
revealGeneration: 7, destination, projectionToken);
|
||
Assert.False(parked.IsCollisionReady);
|
||
Assert.Equal(PlayerState.PortalSpace, controller.State);
|
||
|
||
// A SECOND attempt while still parked must not double-Begin -
|
||
// Runtime's own Begin would refuse a genuine second attempt with
|
||
// Contention, but PrepareDestination's _awaitingPortalWake polls
|
||
// PendingCount instead of re-attempting, so this must also report
|
||
// not-ready without throwing.
|
||
RuntimeDestinationReadiness stillParked =
|
||
projection.PrepareDestination(
|
||
revealGeneration: 7, destination, projectionToken);
|
||
Assert.False(stillParked.IsCollisionReady);
|
||
|
||
// Commit the destination landblock's collision generation and pump
|
||
// the drive's wake (mirrors HeadlessSessionHost.Tick's own
|
||
// PumpFirstEntry -> _acceptedPositionDrive.Advance() ordering).
|
||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||
destinationLandblock, 1UL);
|
||
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;
|
||
runtime.EntityObjects.Physics.Engine.AddLandblock(
|
||
destinationLandblock,
|
||
new TerrainSurface(heights, heightTable),
|
||
[],
|
||
[],
|
||
worldOffsetX: 0f,
|
||
worldOffsetY: 0f);
|
||
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||
destinationLandblock, 1UL, ready: true);
|
||
// RuntimeAcceptedPositionDriveControllerTests.CommitLandblockCollision's
|
||
// exact proven-working shape: the wake path's
|
||
// resolveWorldOffsetFromRuntimeFrame requires the destination
|
||
// landblock's world-frame offset to already be resolvable.
|
||
runtime.EntityObjects.Physics.ObserveLocalWorldFrame(
|
||
destinationLandblock | 0x0001u,
|
||
teleportAdvanced: false);
|
||
// Drain the placement projection FIFO AFTER committing (exact order
|
||
// from RuntimeAcceptedPositionDriveControllerTests.DrainPlacementFifo's
|
||
// call site: commit collision -> drain FIFO -> Advance) - the
|
||
// deferred park's Withdraw notification is published as part of the
|
||
// collision-generation commit, not before it.
|
||
while (runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
|
||
out RuntimePlacementProjectionSnapshot head))
|
||
{
|
||
if (!runtime.EntityObjects.Physics.SetPosition
|
||
.AcknowledgeProjection(head.Token))
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
acceptedPositionDrive.Advance();
|
||
|
||
RuntimeDestinationReadiness committed =
|
||
projection.PrepareDestination(
|
||
revealGeneration: 7, destination, projectionToken);
|
||
Assert.True(committed.IsCollisionReady);
|
||
Assert.Equal(PlayerState.InWorld, controller.State);
|
||
Assert.Equal(10f, controller.Position.X);
|
||
Assert.Equal(10f, controller.Position.Y);
|
||
Assert.Equal(destinationLandblock, controller.CellId & 0xFFFF0000u);
|
||
}
|
||
|
||
/// <summary>
|
||
/// B1 review fix (2026-08-05): headless's required test #2 — the same
|
||
/// unsound-commit-inference defect
|
||
/// <see cref="HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake"/>
|
||
/// proves the HAPPY path for, exercised on the FORGOTTEN path instead. A
|
||
/// DeferredCell park killed by an ordinary, unrelated accepted Position
|
||
/// merge (exactly the ACE 5-10 Hz broadcast <c>RuntimeSetPositionState</c>'s
|
||
/// own doc names as the expected way a far-destination park resolves
|
||
/// without committing) must leave <c>PrepareDestination</c> reporting
|
||
/// NOT ready, <see cref="PlayerState.PortalSpace"/> unchanged, and the
|
||
/// body never moved to the destination - before the fix,
|
||
/// <c>PendingCount</c> hitting 0 made <c>PrepareDestination</c> infer
|
||
/// "committed" and run the full readiness/materialize/LoginComplete
|
||
/// sequence against an unmoved body.
|
||
/// </summary>
|
||
[Fact]
|
||
public void HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted()
|
||
{
|
||
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 = 0x50000009u;
|
||
const uint destinationLandblock = 0xAAB40000u;
|
||
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();
|
||
RuntimeAcceptedPositionDriveController acceptedPositionDrive =
|
||
CreateAcceptedPositionDrive(runtime);
|
||
var projection = new HeadlessSessionWorldProjection(
|
||
runtime,
|
||
collision,
|
||
firstEntry,
|
||
acceptedPositionDrive);
|
||
|
||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||
PlayerMovementController controller =
|
||
Assert.IsType<PlayerMovementController>(
|
||
runtime.MovementOwner.Controller);
|
||
controller.SeedPlacementForTest(
|
||
new Vector3(48f, 49f, 50f),
|
||
0xA9B40001u, new Vector3(48f, 49f, 50f));
|
||
projection.ProjectPosition(
|
||
record,
|
||
isLocalPlayer: true,
|
||
PositionTimestampDisposition.Apply);
|
||
projection.BeginTeleport();
|
||
|
||
var destination = new RuntimeTeleportDestination(
|
||
player,
|
||
InstanceSequence: 1,
|
||
PositionSequence: 2,
|
||
TeleportSequence: 1,
|
||
ForcePositionSequence: 0,
|
||
new Position(
|
||
destinationLandblock | 0x0001u,
|
||
new Vector3(10f, 10f, 50f),
|
||
Quaternion.Identity));
|
||
var projectionToken = new RuntimeWorldHostProjectionToken(
|
||
7, destinationLandblock | 0x0001u);
|
||
|
||
Assert.True(runtime.EntityObjects.TryApplyPosition(
|
||
new WorldSession.EntityPositionUpdate(
|
||
player,
|
||
new CreateObject.ServerPosition(
|
||
destination.Position.ObjCellId,
|
||
destination.Position.Frame.Origin.X,
|
||
destination.Position.Frame.Origin.Y,
|
||
destination.Position.Frame.Origin.Z,
|
||
destination.Position.Frame.Orientation.W,
|
||
destination.Position.Frame.Orientation.X,
|
||
destination.Position.Frame.Orientation.Y,
|
||
destination.Position.Frame.Orientation.Z),
|
||
Velocity: null,
|
||
PlacementId: null,
|
||
IsGrounded: true,
|
||
InstanceSequence: 1,
|
||
PositionSequence: 3,
|
||
TeleportSequence: destination.TeleportSequence,
|
||
ForcePositionSequence: 0),
|
||
isLocalPlayer: true,
|
||
forcePositionRotation: Quaternion.Identity,
|
||
currentLocalVelocity: Vector3.Zero,
|
||
acknowledgeProjection: null,
|
||
out _,
|
||
out _,
|
||
out _));
|
||
|
||
// First attempt: destinationLandblock's collision generation was
|
||
// never begun/committed, so the canonical arm parks DeferredCell.
|
||
RuntimeDestinationReadiness parked = projection.PrepareDestination(
|
||
revealGeneration: 7, destination, projectionToken);
|
||
Assert.False(parked.IsCollisionReady);
|
||
Assert.Equal(PlayerState.PortalSpace, controller.State);
|
||
Assert.Equal(1, acceptedPositionDrive.PendingCount);
|
||
|
||
// An ordinary, UNRELATED accepted Position for the same entity - no
|
||
// new teleport, just a normal broadcast at the SAME already-accepted
|
||
// teleport sequence - Forgets the parked operation the same way
|
||
// ACE's 5-10 Hz cadence would (RuntimeSetPositionState.Forget, called
|
||
// from TryApplyPosition for every accepted, non-Rejected Position).
|
||
Assert.True(runtime.EntityObjects.TryApplyPosition(
|
||
new WorldSession.EntityPositionUpdate(
|
||
player,
|
||
new CreateObject.ServerPosition(
|
||
0x20210001u, 48f, 49f, 50f, 1f, 0f, 0f, 0f),
|
||
Velocity: null,
|
||
PlacementId: null,
|
||
IsGrounded: true,
|
||
InstanceSequence: 1,
|
||
PositionSequence: 4,
|
||
TeleportSequence: destination.TeleportSequence,
|
||
ForcePositionSequence: 0),
|
||
isLocalPlayer: true,
|
||
forcePositionRotation: Quaternion.Identity,
|
||
currentLocalVelocity: Vector3.Zero,
|
||
acknowledgeProjection: null,
|
||
out _,
|
||
out _,
|
||
out _));
|
||
// Forget (inside TryApplyPosition) cancels the underlying
|
||
// RuntimeSetPositionState operation immediately, but the drive's OWN
|
||
// _pending cache only notices on its next Advance() pump - the real
|
||
// host does this every HeadlessSessionHost.Tick via PumpFirstEntry;
|
||
// the test drives it explicitly, same as the App-level equivalent.
|
||
acceptedPositionDrive.Advance();
|
||
Assert.Equal(0, acceptedPositionDrive.PendingCount);
|
||
|
||
// Drive well past where the pre-fix inference would have latched
|
||
// "committed" on the very next PrepareDestination call and then
|
||
// marched to the full readiness/materialize/LoginComplete sequence.
|
||
for (int i = 0; i < 10; i++)
|
||
{
|
||
RuntimeDestinationReadiness stillNotReady =
|
||
projection.PrepareDestination(
|
||
revealGeneration: 7, destination, projectionToken);
|
||
Assert.False(stillNotReady.IsCollisionReady);
|
||
}
|
||
|
||
Assert.Equal(PlayerState.PortalSpace, controller.State);
|
||
}
|
||
|
||
[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.SeedPlacementForTest(
|
||
new Vector3(48f, 49f, 50f),
|
||
0xA9B40001u, new Vector3(48f, 49f, 50f));
|
||
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,
|
||
acknowledgeProjection: null,
|
||
out PositionTimestampDisposition disposition,
|
||
out _,
|
||
out AcceptedPhysicsTimestamps timestamps));
|
||
Assert.Equal(
|
||
PositionTimestampDisposition.ForcePosition,
|
||
disposition);
|
||
|
||
// C4 route 2 (2026-08-03): a ForcePosition on the local player no
|
||
// longer routes through HeadlessSessionWorldProjection.ProjectPosition
|
||
// at all — RuntimeLiveEntitySessionController.OnPositionUpdated
|
||
// dispatches it directly to RuntimeAcceptedPositionDriveController
|
||
// instead (the deleted BlipLocalPlayer's replacement), but R2 review
|
||
// fix (2026-08-03): it re-centers the collision neighborhood on the
|
||
// destination FIRST — the deleted BlipLocalPlayer's own CenterOn
|
||
// side effect, restored via CenterOnAcceptedForcePosition, because a
|
||
// DeferredCell park this neighborhood's window can never publish is
|
||
// a dead end, not a real park
|
||
// (RuntimeAcceptedPositionDriveController.Advance's R1 doc comment).
|
||
projection.CenterOnAcceptedForcePosition(record);
|
||
RuntimeAcceptedPositionDriveController acceptedPositionDrive =
|
||
CreateAcceptedPositionDrive(runtime);
|
||
RuntimeAcceptedPositionExecutionStatus forceStatus =
|
||
acceptedPositionDrive.TryExecuteAcceptedLocalPosition(
|
||
record,
|
||
force,
|
||
disposition,
|
||
timestamps,
|
||
timestamps.PreviousTeleport);
|
||
|
||
Assert.Equal(
|
||
RuntimeAcceptedPositionExecutionStatus.Committed,
|
||
forceStatus);
|
||
// R7 review fix (2026-08-03): Z is 50.005f — within 5 mm of the
|
||
// wire's bare 50f — NOT 50.48f. The dat-exact human Setup's foot
|
||
// sphere is (0,0,0.475) r=.48 (LoadedSetupCollisionSource above);
|
||
// its bottom sits at origin + 0.475 − 0.48 = origin − 0.005, so a
|
||
// settled origin lands 0.005 m ABOVE the floor it rests on (measured
|
||
// empirically against this exact fixture), not a full sphere RADIUS
|
||
// above it. Retail's BlipPlayer (CPhysicsObj::SetPositionSimple
|
||
// @0x005162B0, called from SmartBox::BlipPlayer @0x00453940) has
|
||
// never lifted the origin by a sphere radius — the C4-route-2 FIRST
|
||
// implementation pass (uncommitted; this file asserts a bare 50f at
|
||
// HEAD, never 50.48f) had fitted a 50.48f assertion to a dummy
|
||
// fixture sphere whose offset happened to equal its own radius, not
|
||
// to retail behavior. The comment it carried described the sphere's
|
||
// CENTRE, then wrongly asserted that description about
|
||
// controller.Position, which is the body's ORIGIN
|
||
// (PlayerMovementController.cs -> PhysicsBody.cs Position), not the
|
||
// sphere centre.
|
||
Assert.Equal(new Vector3(72f, 73f, 50.005f), controller.Position);
|
||
// CenterCount is 2: ProjectSpawn's initial centering plus the
|
||
// ForcePosition's own re-centering above (R2's restored mechanism).
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// #365 test 3 — the coverage gap the diagnosis doc's Q4 names
|
||
/// (<c>HeadlessSessionHostTests.cs:347-475</c>'s existing
|
||
/// <see cref="WorldProjectionHydratesCanonicalMovementAndTeleportState"/>
|
||
/// uses <see cref="FixtureCollisionNeighborhood"/>, which is trivially
|
||
/// ready and never opens a real admission — the production
|
||
/// <c>HeadlessCollisionNeighborhood</c> has never been exercised against
|
||
/// the first-entry conductor while an admission was genuinely open).
|
||
/// The player's OWN landblock is prepared the same proven way every
|
||
/// other test in this file does (<c>AddFlatLandblock</c> +
|
||
/// <c>SetPosition.BeginCollisionGeneration</c>/
|
||
/// <c>CommitCollisionGeneration</c>) so it is unconditionally resident —
|
||
/// <see cref="NeighborAdmissionHeldOpenCollisionNeighborhood"/> instead
|
||
/// holds a SEPARATE REAL <see cref="RuntimeCollisionAdmission"/> open on
|
||
/// a DIFFERENT landblock, the same shape as one of the OTHER eight
|
||
/// landblocks in production's real 3x3 publication plan — proving Step
|
||
/// 3a's gate covers "an admission is open ANYWHERE in the plan", not
|
||
/// just the center.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Consolidated-review round (2026-08-10), NIT (c): what THIS bounded
|
||
/// xunit test proves is the GATE, not the #365 hydration stall's
|
||
/// closure — that broader claim's evidence is the separate live-ACE
|
||
/// end-to-end run recorded in
|
||
/// <c>docs/research/2026-08-10-365-headless-hydration-diagnosis.md</c>
|
||
/// §8 ("hydration now succeeds ... entityCount reaches 136"). This
|
||
/// test's own scope is narrower and fully mechanical: the conductor
|
||
/// stays undriven (<c>runtime.MovementOwner.Controller</c> stays null)
|
||
/// for as long as the unrelated admission is held open, and drives and
|
||
/// reaches <c>IsRuntimePublished</c> within a bounded tick budget once
|
||
/// it releases. Temporarily reverting the three <c>IsQuiescent</c>
|
||
/// gates in <c>HeadlessSessionWorldProjection.ProjectSpawn</c>/
|
||
/// <c>ProjectPosition</c>/<c>PumpFirstEntry</c> (never committed — see
|
||
/// §8's "Verification that the new test... actually discriminates")
|
||
/// makes this test fail the SAME way §8 itself observed: the
|
||
/// <c>Assert.Null(runtime.MovementOwner.Controller)</c> checks below
|
||
/// fail because the controller is ALREADY built AND published
|
||
/// (<c>CanExecuteLiveMovement = True</c>) while the unrelated admission
|
||
/// is still open — not merely "reached <c>PublicationCommitted</c>,
|
||
/// still dormant" as an earlier draft of this comment claimed; that
|
||
/// wording described a weaker intermediate state than what reverting
|
||
/// the gate actually produces.
|
||
/// </remarks>
|
||
[Fact]
|
||
public void RealAdmissionNeverDrivesTheConductorWhileOpenAndHydratesOnceReleased()
|
||
{
|
||
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 = 0x50000012u;
|
||
const uint neighborLandblockId = 0xAAB4FFFFu;
|
||
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 NeighborAdmissionHeldOpenCollisionNeighborhood(
|
||
runtime.EntityObjects.Physics,
|
||
neighborLandblockId);
|
||
collision.OpenHeldAdmission();
|
||
var projection = new HeadlessSessionWorldProjection(
|
||
runtime,
|
||
collision,
|
||
firstEntry);
|
||
|
||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||
Assert.False(collision.IsQuiescent);
|
||
Assert.Null(runtime.MovementOwner.Controller);
|
||
|
||
// Hold the SEPARATE admission open across many pumps — far longer
|
||
// than the conductor needs to reach mover-prep/placement/
|
||
// PublicationCommitted on the ALREADY-READY player landblock, so a
|
||
// pre-fix conductor being undriven only by luck of a short window
|
||
// cannot slip through.
|
||
for (int tick = 0; tick < 50; tick++)
|
||
{
|
||
projection.PumpFirstEntry();
|
||
Assert.False(collision.IsQuiescent);
|
||
Assert.Null(runtime.MovementOwner.Controller);
|
||
}
|
||
|
||
collision.ReleaseHeldAdmission();
|
||
Assert.True(collision.IsQuiescent);
|
||
const int boundedTicks = 200;
|
||
bool published = false;
|
||
for (int tick = 0; tick < boundedTicks; tick++)
|
||
{
|
||
projection.PumpFirstEntry();
|
||
if (runtime.MovementOwner.Controller is { IsRuntimePublished: true })
|
||
{
|
||
published = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
Assert.True(
|
||
published,
|
||
"the local player never reached RuntimePublished within the "
|
||
+ "bounded tick budget after the unrelated admission cleared.");
|
||
PlayerMovementController controller = Assert.IsType<
|
||
PlayerMovementController>(runtime.MovementOwner.Controller);
|
||
Assert.True(controller.IsRuntimePublished);
|
||
Assert.Equal(0, firstEntry.PendingCount);
|
||
}
|
||
|
||
/// <summary>
|
||
/// #365 test 4: <c>PumpFirstEntry</c> must not call <c>DriveAll</c> while
|
||
/// the collision neighborhood reports non-quiescent, and must call it on
|
||
/// the first tick after quiescence — the exact Step 3a gate, isolated
|
||
/// from the admission machinery itself via a directly-controllable fake.
|
||
/// </summary>
|
||
[Fact]
|
||
public void PumpFirstEntryWithholdsDriveAllUntilQuiescentThenDrivesImmediately()
|
||
{
|
||
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 = 0x50000013u;
|
||
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 GateControllableCollisionNeighborhood
|
||
{
|
||
QuiescentOverride = false,
|
||
};
|
||
var projection = new HeadlessSessionWorldProjection(
|
||
runtime,
|
||
collision,
|
||
firstEntry);
|
||
|
||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||
Assert.Null(runtime.MovementOwner.Controller);
|
||
Assert.Equal(1, firstEntry.PendingCount);
|
||
|
||
projection.PumpFirstEntry();
|
||
Assert.Null(runtime.MovementOwner.Controller);
|
||
Assert.Equal(1, firstEntry.PendingCount);
|
||
|
||
collision.QuiescentOverride = true;
|
||
projection.PumpFirstEntry();
|
||
|
||
Assert.NotNull(runtime.MovementOwner.Controller);
|
||
}
|
||
|
||
/// <summary>
|
||
/// #365 test 5: <c>HeadlessLocalPlayerFrameHost.CanAdvancePlayer</c>
|
||
/// tracks the controller's exact publication lifecycle — false while
|
||
/// dormant (the crash bug's shape), true once published, false again
|
||
/// once retired (the #356 lifecycle-caller idiom).
|
||
/// </summary>
|
||
[Fact]
|
||
public void CanAdvancePlayerReflectsControllerPublicationLifecycle()
|
||
{
|
||
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);
|
||
Assert.True(runtime.Session.IsInWorld);
|
||
|
||
var inertSession = CreateInertLiveSessionHost();
|
||
var frameHost = new HeadlessLocalPlayerFrameHost(runtime, inertSession);
|
||
|
||
PlayerMovementController candidate =
|
||
PlayerMovementController.CreatePublicationCandidate(
|
||
new PhysicsEngine(),
|
||
PlayerMovementConstructionOptions.Fallback);
|
||
candidate.SealPublicationCandidate();
|
||
candidate.CommitRuntimeOwnership(new RetailObjectQuantumClock());
|
||
runtime.MovementOwner.Controller = candidate;
|
||
Assert.True(candidate.IsRuntimeOwnedDormant);
|
||
Assert.False(frameHost.CanAdvancePlayer);
|
||
|
||
candidate.ActivateRuntimePublication();
|
||
Assert.True(candidate.IsRuntimePublished);
|
||
Assert.True(frameHost.CanAdvancePlayer);
|
||
|
||
candidate.RetireRuntimePublication();
|
||
Assert.False(frameHost.CanAdvancePlayer);
|
||
}
|
||
|
||
private static LiveSessionHost CreateInertLiveSessionHost()
|
||
{
|
||
var controller = new LiveSessionController(
|
||
new ThrowingLiveSessionOperations());
|
||
return new LiveSessionHost(
|
||
controller,
|
||
new LiveSessionHostBindings(
|
||
new LiveSessionRoutingFactories(
|
||
_ => throw new NotSupportedException(),
|
||
_ => throw new NotSupportedException()),
|
||
_ => { },
|
||
new LiveSessionSelectionBindings(
|
||
_ => { },
|
||
_ => { },
|
||
_ => { },
|
||
_ => { },
|
||
_ => { },
|
||
() => { }),
|
||
new LiveSessionEnteredWorldBindings(
|
||
_ => { },
|
||
() => { },
|
||
() => { },
|
||
_ => { },
|
||
() => { }),
|
||
(_, _, _) => { },
|
||
() => { }));
|
||
}
|
||
|
||
private sealed class ThrowingLiveSessionOperations : ILiveSessionOperations
|
||
{
|
||
public IPEndPoint ResolveEndpoint(string host, int port) =>
|
||
throw new NotSupportedException();
|
||
public WorldSession CreateSession(IPEndPoint endpoint) =>
|
||
throw new NotSupportedException();
|
||
public void Connect(
|
||
WorldSession session,
|
||
string user,
|
||
string password) =>
|
||
throw new NotSupportedException();
|
||
public CharacterList.Parsed? GetCharacters(WorldSession session) =>
|
||
throw new NotSupportedException();
|
||
public void EnterWorld(
|
||
WorldSession session,
|
||
int activeCharacterIndex) =>
|
||
throw new NotSupportedException();
|
||
public void Tick(WorldSession session) =>
|
||
throw new NotSupportedException();
|
||
public void DisposeSession(WorldSession session) =>
|
||
throw new NotSupportedException();
|
||
}
|
||
|
||
private static HeadlessSessionDescriptor Descriptor(
|
||
HeadlessCredentialProviderKind provider =
|
||
HeadlessCredentialProviderKind.Environment,
|
||
string credentialReference = "BOT_PASSWORD",
|
||
Dictionary<string, bool>? characterOptions = null) => 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,
|
||
},
|
||
CharacterOptions = characterOptions,
|
||
};
|
||
|
||
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.SeedPlacementForTest(
|
||
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));
|
||
|
||
/// <summary>
|
||
/// C4 route 2 (2026-08-03): mirrors <see cref="CreateFirstEntryDrive"/>'s
|
||
/// construction pattern for the accepted-Position drive controller. No
|
||
/// real WorldSession is needed for these fixture tests —
|
||
/// LocalPlayerOutboundController.SendImmediatePosition no-ops on a null
|
||
/// session.
|
||
/// </summary>
|
||
private static RuntimeAcceptedPositionDriveController
|
||
CreateAcceptedPositionDrive(GameRuntime runtime) => new(
|
||
runtime.EntityObjects,
|
||
runtime.Clock,
|
||
new LoadedSetupCollisionSource(),
|
||
new LocalPlayerOutboundController((_, _, _, _, _, _) => { }),
|
||
() => runtime.Generation,
|
||
() => runtime.PlayerIdentity.ServerGuid,
|
||
() => runtime.MovementOwner.Controller,
|
||
() => runtime.CharacterOwner.UsePositionFromServer,
|
||
() => null);
|
||
|
||
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,
|
||
// R7 review fix (2026-08-03): the dat-exact human Setup
|
||
// 0x02000001 spheres (Ts46SphereListConformanceTests.cs
|
||
// :35-39) — foot (0,0,0.475) r=.48, head/torso
|
||
// (0,0,1.350) r=.48. The PREVIOUS single dummy sphere at
|
||
// (0,0,0) r=.48 (offset == radius) made a settled origin
|
||
// rest a FULL radius above the floor; the real foot
|
||
// sphere's bottom is origin + 0.475 − 0.48 = origin −
|
||
// 0.005, so a settled origin lands ON the floor within
|
||
// 5 mm. Retail's BlipPlayer has never lifted the origin
|
||
// by a sphere radius — that was a fixture artifact, not
|
||
// a retail-fidelity gain (docs/ISSUES.md #285 correction).
|
||
[
|
||
new FlatCollisionSphere(
|
||
new Vector3(0f, 0f, 0.475f), 0.48f),
|
||
new FlatCollisionSphere(
|
||
new Vector3(0f, 0f, 1.350f), 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()
|
||
{
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// D1 (C5b architecture review), the local-player FORCE arm — both
|
||
/// directions of the reachability rule, driven through the real
|
||
/// <see cref="RuntimeLiveEntitySessionController"/> sink.
|
||
///
|
||
/// <para>
|
||
/// The graphical <c>OnPosition</c> route returns ahead of AD-60's W2
|
||
/// for every force status EXCEPT <c>NotApplicable</c>, and falls
|
||
/// through to it on that one. So: a force the drive HANDLED
|
||
/// (<c>Committed</c> here) is placement-receipt-authoritative and this
|
||
/// route must leave residency at the cell the placement RESOLVED, not
|
||
/// re-stamp the wire cell over it; a force the drive did NOT handle
|
||
/// (no drive at all — the login-window / route-1 shape) must still
|
||
/// refresh the cell, because nothing else will.
|
||
/// </para>
|
||
/// </summary>
|
||
[Theory]
|
||
[InlineData(true)]
|
||
[InlineData(false)]
|
||
public void LocalForcePosition_CommitsTheWireCellOnlyWhenTheDriveDeclined(
|
||
bool driveHandlesIt)
|
||
{
|
||
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 = 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);
|
||
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.SeedPlacementForTest(
|
||
new Vector3(48f, 49f, 50f),
|
||
0xA9B40001u,
|
||
new Vector3(48f, 49f, 50f));
|
||
// The residence must be closed before the ordinary post-residence
|
||
// rules apply at all (the conductor is the sole cell authority
|
||
// while it is open — asserted separately in the Runtime suite).
|
||
Assert.False(runtime.EntityObjects.TryGetInitialCreateResidence(
|
||
record,
|
||
out _));
|
||
|
||
using var session = new WorldSession(
|
||
new IPEndPoint(IPAddress.Loopback, 9000));
|
||
var entities = new RuntimeLiveEntitySessionController(
|
||
runtime,
|
||
session,
|
||
log: null,
|
||
projection,
|
||
driveHandlesIt ? CreateAcceptedPositionDrive(runtime) : null);
|
||
LiveEntitySessionSink sink = entities.CreateSink();
|
||
|
||
// A wire cell that is NOT the cell a placement at (72,73) resolves
|
||
// to, so "committed the wire cell" and "kept the placement's own
|
||
// resolved cell" are distinguishable values rather than the same
|
||
// number arrived at two ways.
|
||
const uint wireCell = 0xA9B40002u;
|
||
sink.PositionUpdated(new WorldSession.EntityPositionUpdate(
|
||
player,
|
||
new CreateObject.ServerPosition(
|
||
wireCell, 72f, 73f, 50f, 1f, 0f, 0f, 0f),
|
||
Velocity: null,
|
||
PlacementId: null,
|
||
IsGrounded: true,
|
||
InstanceSequence: 1,
|
||
PositionSequence: 2,
|
||
TeleportSequence: 0,
|
||
ForcePositionSequence: 1));
|
||
|
||
if (driveHandlesIt)
|
||
{
|
||
// The placement committed and resolved its OWN cell — measured
|
||
// 0xA9B4001C, the outdoor landcell that actually contains
|
||
// (72, 73) in this flat landblock, which is neither the wire
|
||
// cell nor the spawn cell. This route added nothing on top of
|
||
// it.
|
||
Assert.Equal(new Vector3(72f, 73f, 50.005f), controller.Position);
|
||
Assert.Equal(0xA9B4001Cu, record.FullCellId);
|
||
Assert.NotEqual(wireCell, record.FullCellId);
|
||
}
|
||
else
|
||
{
|
||
Assert.Equal(wireCell, record.FullCellId);
|
||
Assert.Equal(0xA9B4FFFFu, record.CanonicalLandblockId);
|
||
}
|
||
}
|
||
|
||
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)));
|
||
|
||
// SF-4 fixture: minimal PlayerDescription (0x0013) body carrying only
|
||
// the CharacterOptions1/2 trailer fields — copied from
|
||
// HeadlessCharacterOptionsSeederWiringTests.WrapPlayerDescriptionEnvelope
|
||
// (mirrors GameEventWiringTests.WireAll_PlayerDescription_PublishesCharacterOptions's
|
||
// fixture layout).
|
||
private static byte[] WrapPlayerDescriptionEnvelope(
|
||
uint options1,
|
||
uint options2)
|
||
{
|
||
var stream = new MemoryStream();
|
||
using (var writer = new BinaryWriter(
|
||
stream, System.Text.Encoding.UTF8, leaveOpen: true))
|
||
{
|
||
writer.Write(0u); // property flags
|
||
writer.Write(0x52u); // player weenie type
|
||
writer.Write(0u); // vector flags
|
||
writer.Write(0u); // has health
|
||
writer.Write(0x40u); // option flags: CharacterOptions2
|
||
writer.Write(options1);
|
||
writer.Write(0u); // legacy hotbar count
|
||
writer.Write(0u); // spellbook filters
|
||
writer.Write(options2);
|
||
writer.Write(0u); // inventory count
|
||
writer.Write(0u); // equipped count
|
||
}
|
||
|
||
byte[] payload = stream.ToArray();
|
||
byte[] body = new byte[GameEventEnvelope.HeaderSize + payload.Length];
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body, GameEventEnvelope.Opcode);
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), 0u);
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), 0u);
|
||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||
body.AsSpan(12), (uint)GameEventType.PlayerDescription);
|
||
Array.Copy(payload, 0, body, GameEventEnvelope.HeaderSize, payload.Length);
|
||
return body;
|
||
}
|
||
|
||
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;
|
||
|
||
// #365 Step 3a: this fixture has no real admission/publication
|
||
// machinery to hold open — every CenterOn/IsReady call is a
|
||
// synchronous no-op, so it is quiescent by construction. A fake
|
||
// that ever wants to exercise the gate should override this.
|
||
public bool IsQuiescent => true;
|
||
|
||
// 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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// #365 test 3 support: unlike <see cref="FixtureCollisionNeighborhood"/>
|
||
/// (trivially ready, never opens a real admission),
|
||
/// <see cref="RealAdmissionNeverDrivesTheConductorWhileOpenAndHydratesOnceReleased"/>
|
||
/// needs a fake that holds an ACTUAL <see cref="RuntimeCollisionAdmission"/>
|
||
/// open on a landblock the local player does NOT target — the same
|
||
/// per-landblock admission primitive production's
|
||
/// <c>HeadlessCollisionNeighborhood.CreatePublication</c> opens for every
|
||
/// entry in its 3x3 plan. Cancelling (never committing) sidesteps
|
||
/// <c>CommitCollisionGeneration</c>'s own multi-tick
|
||
/// <c>TryAcquireCollisionPrefixMutationPermission</c> settlement — this
|
||
/// fake only needs to prove the SEAL sees an open admission, not drive a
|
||
/// second full commit cycle to completion.
|
||
/// </summary>
|
||
private sealed class NeighborAdmissionHeldOpenCollisionNeighborhood(
|
||
RuntimePhysicsState physics,
|
||
uint heldLandblockId) : IHeadlessCollisionNeighborhood
|
||
{
|
||
private RuntimeCollisionAdmission? _admission;
|
||
private PreparedLandblockCollisionGeneration? _prepared;
|
||
|
||
internal void OpenHeldAdmission()
|
||
{
|
||
_admission = physics.BeginCollisionAdmission(heldLandblockId);
|
||
_prepared = physics.PrepareCollisionGeneration(_admission);
|
||
}
|
||
|
||
internal void ReleaseHeldAdmission()
|
||
{
|
||
if (_admission is null)
|
||
return;
|
||
bool cancelled = physics.CancelCollisionGeneration(
|
||
_admission,
|
||
_prepared);
|
||
Assert.True(
|
||
cancelled,
|
||
"the held-open neighbor admission did not cancel in one call.");
|
||
_admission = null;
|
||
_prepared = null;
|
||
}
|
||
|
||
public bool IsQuiescent => _admission is null;
|
||
|
||
public void CenterOn(uint fullCellId)
|
||
{
|
||
}
|
||
|
||
public bool IsReady(uint fullCellId) => true;
|
||
|
||
public bool IsWithinServiceWindow(uint fullCellId) => true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// #365 test 4 support: a fake whose <see cref="IsQuiescent"/> the test
|
||
/// flips directly, isolating <c>PumpFirstEntry</c>'s gate from any real
|
||
/// admission machinery.
|
||
/// </summary>
|
||
private sealed class GateControllableCollisionNeighborhood
|
||
: IHeadlessCollisionNeighborhood
|
||
{
|
||
private uint _lastCell;
|
||
|
||
internal bool QuiescentOverride { get; set; } = true;
|
||
|
||
public void CenterOn(uint fullCellId) => _lastCell = fullCellId;
|
||
|
||
public bool IsReady(uint fullCellId) => fullCellId == _lastCell;
|
||
|
||
public bool IsWithinServiceWindow(uint fullCellId) => true;
|
||
|
||
public bool IsQuiescent => QuiescentOverride;
|
||
}
|
||
|
||
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");
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|