Campaign P remaining-physics-divergence, placement cutover slice C3c
(docs/plans/2026-08-02-placement-cutover.md). Both production hosts now
register every initial Create through the residence + continuation-
executor + first-entry-conductor machinery (C0-C3b):
- Graphical (route 1): RegisterEntityWithInitialResidence at Create; the
shared RuntimeFirstEntryDriveController pumps both conductors from the
placement-receipt flow; MaterializeProjection and RebucketLiveEntity
are presentation-only while a residence is ACTIVE (ExecutorCompleted is
the presentation-binding receipt); post-residence entities take the
full legacy path including the prepare_to_enter_world clock edges.
PlayerModeController attaches presentation to the Runtime-published
controller; its legacy resolve/step-heights/host-construction path is
deleted; presentation-only rollback (retail has no entry-flow rollback).
- Headless (route 8): OnSpawned registers with residence when a drive
exists; content-less sessions keep the pre-flip direct registration;
SynchronizeLocalPlayer/CreateController/ApplySetupStepHeights deleted;
prepared-collision read failure is a typed AwaitingCollisionSource
retry; far remotes outside the service window complete celless.
- RuntimeLocalPlayerMovementState.Controller setter sealed internal; all
controller mutation flows through the publication lifecycle.
Fix slices landed within this cutover, each dual-gated:
- F1: live movement-stat/server-physics application routed through the
Runtime ownership seam (post-logout ingest crash on the retired
controller eliminated; RuntimeMovementSkillProjection deleted).
- F2: login activation wedge - collision-admission prefix gate factored
out of the seal (reentrant-commit RejectedAuthority), rearm generation
identity corrected, PlayerModeAutoEntry requires the Runtime-published
controller (world reveal can no longer seal unmaterialized).
- F3: landblock-prefix 0-sentinel replaced by explicit absent-id guards;
map-corner landblocks (grid row/col 0) fully legal through admission,
park/rearm/retire, quiescence, and outdoor shadow seeds.
- F5: local-player first-entry ground contact seeded by the shared
SpawnPlacementSettler (moved App->Core) at FinalizeActivation - the
retail first-gravity-frame touch (enter_world 0x00516170 carries no
seed); the legacy unconditional force-seed is overwritten by a real
floor-found contact; airborne spawns stay airborne; outbound contact
bit verified end-to-end. Fixes the standing-cast 'You can't do that
while in the air!' rejections.
- R1 (dual-review round): login constraint leash armed at the committed
placement (HandleReceivedPosition 0x00453FD0 analog); register rows
AD-61 (settle-timing compression now covering the local player) and
AD-42 (repointed off the deleted resolve split) in this commit;
residence-conversion owner API; wire-landblock guards; drive-pending
ledger in IsConverged; route attach/detach latch; executor-drain drift
model documented + source-pinned.
Gates: Runtime 1,003, App 4,039/3 skips, Headless 79, complete solution
10,816/0 failed/4 skips (Release, -m:1); connected lifecycle/reconnect
gate PASS (logs/connected-world-gate-20260802-175401; graceful exits,
world-visible, zero airborne rejections). The nine-stop soak remains red
for the pre-existing 6b28ff99 whole-world collision-clone throughput
regression (attributed with evidence; scheduled as its own slice before
C5). Dual Opus reviews (retail-conformance + adversarial): delta PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
450 lines
16 KiB
C#
450 lines
16 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Physics;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Runtime.Entities;
|
|
|
|
namespace AcDream.App.Tests.Physics;
|
|
|
|
public sealed class LiveEntityInboundAuthorityGateTests
|
|
{
|
|
private const uint Guid = 0x50000021u;
|
|
|
|
[Fact]
|
|
public void Motion_StrictFreshnessAndWrapAreOwnedBeforePresentation()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 7, movement: 0xFFFE));
|
|
var published = new List<AcceptedPhysicsTimestamps>();
|
|
var gate = new LiveEntityInboundAuthorityGate(
|
|
runtime,
|
|
(_, timestamps) => published.Add(timestamps));
|
|
|
|
Assert.False(gate.TryAcceptMotion(
|
|
Motion(instance: 7, movement: 0xFFFE, server: 1),
|
|
retainPayload: true,
|
|
out _,
|
|
out bool equalAccepted));
|
|
Assert.False(equalAccepted);
|
|
|
|
Assert.True(gate.TryAcceptMotion(
|
|
Motion(instance: 7, movement: 1, server: 1),
|
|
retainPayload: true,
|
|
out AcceptedMotionNetworkUpdate wrapped,
|
|
out bool wrapAccepted));
|
|
Assert.True(wrapAccepted);
|
|
Assert.Equal((ulong)2, wrapped.Record.MovementAuthorityVersion);
|
|
Assert.Single(published);
|
|
|
|
Assert.False(gate.TryAcceptMotion(
|
|
Motion(instance: 7, movement: 1, server: 1),
|
|
retainPayload: true,
|
|
out _,
|
|
out bool duplicateAccepted));
|
|
Assert.False(duplicateAccepted);
|
|
Assert.Single(published);
|
|
}
|
|
|
|
[Fact]
|
|
public void AutonomousLocalMotion_ConsumesTimestampButDoesNotPublishPayload()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 1));
|
|
int publishCount = 0;
|
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => publishCount++);
|
|
|
|
Assert.False(gate.TryAcceptMotion(
|
|
Motion(instance: 1, movement: 2, server: 1, autonomous: true),
|
|
retainPayload: false,
|
|
out _,
|
|
out bool timestampAccepted));
|
|
|
|
Assert.True(timestampAccepted);
|
|
Assert.Equal(1, publishCount);
|
|
Assert.True(runtime.TryGetRecord(Guid, out LiveEntityRecord record));
|
|
Assert.Equal((ulong)1, record.MovementAuthorityVersion);
|
|
}
|
|
|
|
[Fact]
|
|
public void Vector_InvalidPayloadDoesNotConsumeSequenceAndDuplicateIsRejected()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 2));
|
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
|
var update = new VectorUpdate.Parsed(
|
|
Guid,
|
|
new Vector3(1f, 2f, 3f),
|
|
Vector3.Zero,
|
|
InstanceSequence: 2,
|
|
VectorSequence: 2);
|
|
|
|
Assert.False(gate.TryAcceptVector(update, payloadIsValid: false, out _));
|
|
Assert.True(gate.TryAcceptVector(
|
|
update,
|
|
payloadIsValid: true,
|
|
out AcceptedVectorNetworkUpdate accepted));
|
|
Assert.Equal((ulong)2, accepted.VectorAuthorityVersion);
|
|
Assert.False(gate.TryAcceptVector(update, payloadIsValid: true, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void Vector_WrappedSequenceIsAccepted()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 2, vector: 0xFFFE));
|
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
|
|
|
Assert.True(gate.TryAcceptVector(
|
|
new VectorUpdate.Parsed(
|
|
Guid,
|
|
Vector3.One,
|
|
Vector3.Zero,
|
|
InstanceSequence: 2,
|
|
VectorSequence: 1),
|
|
payloadIsValid: true,
|
|
out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void State_EqualIsRejectedAndWrappedSequenceIsAccepted()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 3, state: 0xFFFE));
|
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
|
|
|
Assert.False(gate.TryAcceptState(
|
|
new SetState.Parsed(Guid, (uint)PhysicsStateFlags.Hidden, 3, 0xFFFE),
|
|
out _));
|
|
Assert.True(gate.TryAcceptState(
|
|
new SetState.Parsed(Guid, (uint)PhysicsStateFlags.Hidden, 3, 1),
|
|
out AcceptedStateNetworkUpdate accepted));
|
|
Assert.Equal((ulong)2, accepted.StateAuthorityVersion);
|
|
Assert.False(gate.TryAcceptState(
|
|
new SetState.Parsed(Guid, 0, 3, 1),
|
|
out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void Position_UnknownLocalHintAndResetDoNotConsumeFuturePacket()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
int publishCount = 0;
|
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => publishCount++);
|
|
WorldSession.EntityPositionUpdate update = Position(
|
|
Guid,
|
|
instance: 4,
|
|
position: 2,
|
|
cell: 0xAABB0001u);
|
|
|
|
Assert.False(gate.TryAcceptPosition(
|
|
update,
|
|
Guid,
|
|
Quaternion.Identity,
|
|
Vector3.Zero,
|
|
payloadIsValid: true,
|
|
out _));
|
|
Assert.Equal(0xAABB0001u, gate.LastLivePlayerLandblockId);
|
|
Assert.Equal(0, publishCount);
|
|
|
|
Register(runtime, Spawn(Guid, instance: 4, position: 1));
|
|
Assert.True(gate.TryAcceptPosition(
|
|
update,
|
|
Guid,
|
|
Quaternion.Identity,
|
|
Vector3.Zero,
|
|
payloadIsValid: true,
|
|
out AcceptedPositionNetworkUpdate accepted));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, accepted.TimestampDisposition);
|
|
Assert.Equal((ulong)2, accepted.PositionAuthorityVersion);
|
|
Assert.True(runtime.TryGetCanonical(Guid, out RuntimeEntityRecord canonical));
|
|
Assert.Same(canonical, accepted.Canonical);
|
|
Assert.Equal(1, publishCount);
|
|
|
|
gate.ResetSessionState();
|
|
Assert.Null(gate.LastLivePlayerLandblockId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Position_InvalidPayloadDoesNotConsumeSequence()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 5, position: 1));
|
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
|
WorldSession.EntityPositionUpdate update = Position(Guid, 5, 2, 0x01010001u);
|
|
|
|
Assert.False(gate.TryAcceptPosition(
|
|
update, Guid, Quaternion.Identity, Vector3.Zero, false, out _));
|
|
Assert.True(gate.TryAcceptPosition(
|
|
update, Guid, Quaternion.Identity, Vector3.Zero, true, out _));
|
|
Assert.False(gate.TryAcceptPosition(
|
|
update, Guid, Quaternion.Identity, Vector3.Zero, true, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void Position_WrappedSequenceIsAccepted()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 5, position: 0xFFFE));
|
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
|
|
|
Assert.True(gate.TryAcceptPosition(
|
|
Position(Guid, 5, 1, 0x01010002u),
|
|
Guid,
|
|
Quaternion.Identity,
|
|
Vector3.Zero,
|
|
payloadIsValid: true,
|
|
out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void Position_CanonicalInventoryObjectIsAcceptedBeforeProjectionExists()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
LiveEntityRegistrationResult registration =
|
|
runtime.RegisterLiveEntity(Spawn(Guid, instance: 5, position: 1));
|
|
Assert.NotNull(registration.Canonical);
|
|
Assert.False(runtime.TryGetRecord(Guid, out _));
|
|
int publishCount = 0;
|
|
var gate = new LiveEntityInboundAuthorityGate(
|
|
runtime,
|
|
(_, _) => publishCount++);
|
|
|
|
Assert.True(gate.TryAcceptPosition(
|
|
Position(Guid, 5, 2, 0x01010002u),
|
|
localPlayerGuid: 0u,
|
|
forcePositionRotation: null,
|
|
currentLocalVelocity: null,
|
|
payloadIsValid: true,
|
|
out AcceptedPositionNetworkUpdate accepted));
|
|
|
|
Assert.Same(registration.Canonical, accepted.Canonical);
|
|
// C3c: the accepted Position of an entity whose initial-create
|
|
// residence is still pending is admitted into the residence FIFO —
|
|
// its merge (and the position-authority advance) commits at the
|
|
// executor drain, so the gate's captured authority is the
|
|
// admission-time version, not a post-apply bump.
|
|
Assert.Equal((ulong)1, accepted.PositionAuthorityVersion);
|
|
Assert.Equal(1, publishCount);
|
|
Assert.False(runtime.TryGetRecord(Guid, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void Vector_NewerSameIncarnationPacketInvalidatesCapturedAuthority()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 5, vector: 1));
|
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
|
Assert.True(gate.TryAcceptVector(
|
|
new VectorUpdate.Parsed(Guid, Vector3.One, Vector3.Zero, 5, 2),
|
|
payloadIsValid: true,
|
|
out AcceptedVectorNetworkUpdate accepted));
|
|
|
|
Assert.True(runtime.TryApplyVector(
|
|
new VectorUpdate.Parsed(Guid, Vector3.UnitX, Vector3.Zero, 5, 3),
|
|
out _));
|
|
Assert.False(runtime.IsCurrentVectorAuthority(
|
|
accepted.Record,
|
|
accepted.VectorAuthorityVersion));
|
|
}
|
|
|
|
[Fact]
|
|
public void State_NewerSameIncarnationPacketInvalidatesCapturedAuthority()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 5, state: 1));
|
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
|
Assert.True(gate.TryAcceptState(
|
|
new SetState.Parsed(Guid, (uint)PhysicsStateFlags.Hidden, 5, 2),
|
|
out AcceptedStateNetworkUpdate accepted));
|
|
|
|
Assert.True(runtime.TryApplyState(
|
|
new SetState.Parsed(Guid, 0, 5, 3),
|
|
out _));
|
|
Assert.False(runtime.IsCurrentStateAuthority(
|
|
accepted.Record,
|
|
accepted.StateAuthorityVersion));
|
|
}
|
|
|
|
[Fact]
|
|
public void Motion_PublisherReplacementInvalidatesCapturedIncarnation()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 6));
|
|
var gate = new LiveEntityInboundAuthorityGate(
|
|
runtime,
|
|
(_, _) => runtime.RegisterLiveEntity(Spawn(Guid, instance: 7)));
|
|
|
|
Assert.False(gate.TryAcceptMotion(
|
|
Motion(instance: 6, movement: 2, server: 1),
|
|
retainPayload: true,
|
|
out _,
|
|
out bool timestampAccepted));
|
|
Assert.True(timestampAccepted);
|
|
Assert.True(runtime.TryGetCanonical(Guid, out RuntimeEntityRecord replacement));
|
|
Assert.Equal((ushort)7, replacement.Incarnation);
|
|
Assert.NotNull(replacement.LocalEntityId);
|
|
Assert.False(runtime.TryGetRecord(Guid, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void Position_PublisherNewerChannelInvalidatesCapturedAuthority()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 8, position: 1));
|
|
var nested = Position(Guid, 8, 3, 0x01010002u);
|
|
LiveEntityInboundAuthorityGate? gate = null;
|
|
gate = new LiveEntityInboundAuthorityGate(
|
|
runtime,
|
|
(_, _) => runtime.TryApplyPosition(
|
|
nested,
|
|
isLocalPlayer: true,
|
|
forcePositionRotation: Quaternion.Identity,
|
|
currentLocalVelocity: Vector3.Zero,
|
|
out _,
|
|
out _,
|
|
out _));
|
|
|
|
Assert.False(gate.TryAcceptPosition(
|
|
Position(Guid, 8, 2, 0x01010001u),
|
|
Guid,
|
|
Quaternion.Identity,
|
|
Vector3.Zero,
|
|
payloadIsValid: true,
|
|
out _));
|
|
Assert.True(runtime.TryGetRecord(Guid, out LiveEntityRecord record));
|
|
Assert.Equal((ulong)3, record.PositionAuthorityVersion);
|
|
}
|
|
|
|
[Fact]
|
|
public void Position_PublisherNewerVectorPreservesIndependentPositionAuthority()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
Register(runtime, Spawn(Guid, instance: 9, position: 1, vector: 1));
|
|
var gate = new LiveEntityInboundAuthorityGate(
|
|
runtime,
|
|
(_, _) => runtime.TryApplyVector(
|
|
new VectorUpdate.Parsed(
|
|
Guid,
|
|
Vector3.UnitX,
|
|
Vector3.Zero,
|
|
InstanceSequence: 9,
|
|
VectorSequence: 2),
|
|
out _));
|
|
|
|
Assert.True(gate.TryAcceptPosition(
|
|
Position(Guid, 9, 2, 0x01010003u),
|
|
Guid,
|
|
Quaternion.Identity,
|
|
Vector3.Zero,
|
|
payloadIsValid: true,
|
|
out AcceptedPositionNetworkUpdate accepted));
|
|
Assert.True(runtime.IsCurrentPositionAuthority(
|
|
accepted.Canonical,
|
|
accepted.PositionAuthorityVersion));
|
|
Assert.False(runtime.IsCurrentVelocityAuthority(
|
|
accepted.Canonical,
|
|
accepted.VelocityAuthorityVersion));
|
|
}
|
|
|
|
private static LiveEntityRuntime Runtime() => LiveEntityRuntimeFixture.Create(
|
|
new GpuWorldState(),
|
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
|
|
|
private static LiveEntityRecord Register(
|
|
LiveEntityRuntime runtime,
|
|
WorldSession.EntitySpawn spawn) =>
|
|
runtime.RegisterAndMaterializeProjection(spawn);
|
|
|
|
private static WorldSession.EntityMotionUpdate Motion(
|
|
ushort instance,
|
|
ushort movement,
|
|
ushort server,
|
|
bool autonomous = false) => new(
|
|
Guid,
|
|
default,
|
|
instance,
|
|
movement,
|
|
server,
|
|
autonomous);
|
|
|
|
private static WorldSession.EntityPositionUpdate Position(
|
|
uint guid,
|
|
ushort instance,
|
|
ushort position,
|
|
uint cell) => new(
|
|
guid,
|
|
new CreateObject.ServerPosition(
|
|
cell, 10f, 11f, 12f, 1f, 0f, 0f, 0f),
|
|
Velocity: null,
|
|
PlacementId: null,
|
|
IsGrounded: true,
|
|
InstanceSequence: instance,
|
|
PositionSequence: position,
|
|
TeleportSequence: 0,
|
|
ForcePositionSequence: 0);
|
|
|
|
private static WorldSession.EntitySpawn Spawn(
|
|
uint guid,
|
|
ushort instance,
|
|
ushort position = 1,
|
|
ushort movement = 1,
|
|
ushort state = 1,
|
|
ushort vector = 1)
|
|
{
|
|
var serverPosition = new CreateObject.ServerPosition(
|
|
0x01010001u, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
|
|
var timestamps = new PhysicsTimestamps(
|
|
position,
|
|
movement,
|
|
state,
|
|
vector,
|
|
Teleport: 0,
|
|
ServerControlledMove: 1,
|
|
ForcePosition: 0,
|
|
ObjDesc: 1,
|
|
Instance: instance);
|
|
var physics = new PhysicsSpawnData(
|
|
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
|
Position: serverPosition,
|
|
Movement: null,
|
|
AnimationFrame: null,
|
|
SetupTableId: 0x02000001u,
|
|
MotionTableId: 0x09000001u,
|
|
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,
|
|
serverPosition,
|
|
0x02000001u,
|
|
Array.Empty<CreateObject.AnimPartChange>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
null,
|
|
null,
|
|
"fixture",
|
|
null,
|
|
null,
|
|
0x09000001u,
|
|
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
|
|
InstanceSequence: instance,
|
|
MovementSequence: movement,
|
|
ServerControlSequence: 1,
|
|
PositionSequence: position,
|
|
Physics: physics);
|
|
}
|
|
}
|