Move the presentation-free inbound physics timestamp/snapshot authority and parent-relation state into AcDream.Runtime.Entities without changing their control flow. Move their dedicated tests with them and keep App consumers as borrowers during the staged J3 cutover. Validated by 26 focused Runtime tests, 232 focused App tests, the Release solution build, and 8,429 complete Release tests with five existing skips. Co-authored-by: Codex <codex@openai.com>
409 lines
14 KiB
C#
409 lines
14 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();
|
|
runtime.RegisterLiveEntity(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();
|
|
runtime.RegisterLiveEntity(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();
|
|
runtime.RegisterLiveEntity(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();
|
|
runtime.RegisterLiveEntity(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();
|
|
runtime.RegisterLiveEntity(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);
|
|
|
|
runtime.RegisterLiveEntity(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.Equal(1, publishCount);
|
|
|
|
gate.ResetSessionState();
|
|
Assert.Null(gate.LastLivePlayerLandblockId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Position_InvalidPayloadDoesNotConsumeSequence()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
runtime.RegisterLiveEntity(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();
|
|
runtime.RegisterLiveEntity(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 Vector_NewerSameIncarnationPacketInvalidatesCapturedAuthority()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
runtime.RegisterLiveEntity(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();
|
|
runtime.RegisterLiveEntity(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();
|
|
runtime.RegisterLiveEntity(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.TryGetRecord(Guid, out LiveEntityRecord replacement));
|
|
Assert.Equal((ushort)7, replacement.Generation);
|
|
}
|
|
|
|
[Fact]
|
|
public void Position_PublisherNewerChannelInvalidatesCapturedAuthority()
|
|
{
|
|
LiveEntityRuntime runtime = Runtime();
|
|
runtime.RegisterLiveEntity(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();
|
|
runtime.RegisterLiveEntity(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.Record,
|
|
accepted.PositionAuthorityVersion));
|
|
Assert.False(runtime.IsCurrentVelocityAuthority(
|
|
accepted.Record,
|
|
accepted.VelocityAuthorityVersion));
|
|
}
|
|
|
|
private static LiveEntityRuntime Runtime() => new(
|
|
new GpuWorldState(),
|
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
|
|
|
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);
|
|
}
|
|
}
|