acdream/tests/AcDream.App.Tests/Physics/LiveEntityInboundAuthorityGateTests.cs
Erik 529e0e9d88 feat(runtime): C3c - production placement cutover: both hosts on the residence conductors (routes 1+8)
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>
2026-08-02 18:10:33 +02:00

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);
}
}