Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
492 lines
18 KiB
C#
492 lines
18 KiB
C#
using System.Numerics;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
|
|
namespace AcDream.App.Tests.World;
|
|
|
|
public sealed class InboundPhysicsStateControllerTests
|
|
{
|
|
[Fact]
|
|
public void SameGenerationCreate_RoutesChannelsIndependently()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
CreateObject.ServerPosition original = Position(0x0101FFFFu, 10f);
|
|
CreateObject.ServerPosition incoming = Position(0x0101FFFFu, 20f);
|
|
controller.AcceptCreate(Spawn(0x70000001u, 1, 10, 10, original, 0x408u));
|
|
|
|
InboundCreateResult refresh = controller.AcceptCreate(
|
|
Spawn(0x70000001u, 1, 9, 11, incoming, 0x448u));
|
|
|
|
Assert.Equal(CreateObjectTimestampDisposition.ExistingGeneration, refresh.Disposition);
|
|
Assert.NotNull(refresh.SameGenerationEvents);
|
|
SameGenerationCreateObjectEvents events = refresh.SameGenerationEvents.Value;
|
|
Assert.True(controller.TryApplyPosition(
|
|
events.Position!.Value,
|
|
isLocalPlayer: false,
|
|
forcePositionRotation: null,
|
|
currentLocalVelocity: null,
|
|
out PositionTimestampDisposition positionResult,
|
|
out _,
|
|
out _));
|
|
Assert.Equal(PositionTimestampDisposition.Rejected, positionResult);
|
|
Assert.True(controller.TryApplyState(events.State, out _));
|
|
Assert.True(controller.TryGetSnapshot(0x70000001u, out WorldSession.EntitySpawn accepted));
|
|
Assert.Equal(original, accepted.Position);
|
|
Assert.Equal(0x448u, accepted.PhysicsState);
|
|
}
|
|
|
|
[Fact]
|
|
public void PickupRetainsGeneration_AndFreshPositionReturnsToWorld()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
controller.AcceptCreate(Spawn(
|
|
0x70000002u, 7, 20, 1, Position(0x0101FFFFu, 10f), 0x408u));
|
|
|
|
Assert.True(controller.TryApplyPickup(
|
|
new PickupEvent.Parsed(0x70000002u, 7, 21), out WorldSession.EntitySpawn pickedUp));
|
|
Assert.Null(pickedUp.Position);
|
|
|
|
var update = new WorldSession.EntityPositionUpdate(
|
|
0x70000002u,
|
|
Position(0x0101FFFFu, 30f),
|
|
null,
|
|
null,
|
|
true,
|
|
7,
|
|
22,
|
|
0,
|
|
0);
|
|
Assert.True(controller.TryApplyPosition(
|
|
update, false, null, null, out var disposition, out WorldSession.EntitySpawn returned, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
Assert.Equal(30f, returned.Position!.Value.PositionX);
|
|
}
|
|
|
|
[Fact]
|
|
public void PositionPlacementAbsentAndPresentZeroBothApplyRetailZero()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
WorldSession.EntitySpawn seed = Spawn(
|
|
0x70000009u, 7, 20, 1, Position(0x0101FFFFu, 10f), 0x408u);
|
|
seed = seed with
|
|
{
|
|
PlacementId = 7,
|
|
Physics = seed.Physics!.Value with { AnimationFrame = 7 },
|
|
};
|
|
controller.AcceptCreate(seed);
|
|
|
|
Assert.True(controller.TryApplyPosition(
|
|
new WorldSession.EntityPositionUpdate(
|
|
seed.Guid,
|
|
Position(0x0101FFFFu, 20f),
|
|
null,
|
|
null,
|
|
true,
|
|
7,
|
|
21,
|
|
0,
|
|
0),
|
|
false,
|
|
null,
|
|
null,
|
|
out PositionTimestampDisposition firstDisposition,
|
|
out WorldSession.EntitySpawn first,
|
|
out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, firstDisposition);
|
|
Assert.Equal((uint)0, first.PlacementId);
|
|
Assert.Equal((uint)0, first.Physics!.Value.AnimationFrame);
|
|
|
|
Assert.True(controller.TryApplyPosition(
|
|
new WorldSession.EntityPositionUpdate(
|
|
seed.Guid,
|
|
Position(0x0101FFFFu, 30f),
|
|
null,
|
|
0,
|
|
true,
|
|
7,
|
|
22,
|
|
0,
|
|
0),
|
|
false,
|
|
null,
|
|
null,
|
|
out PositionTimestampDisposition secondDisposition,
|
|
out WorldSession.EntitySpawn second,
|
|
out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, secondDisposition);
|
|
Assert.Equal((uint)0, second.PlacementId);
|
|
Assert.Equal((uint)0, second.Physics!.Value.AnimationFrame);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemotePositionWithoutVelocityAppliesUnpackedZeroVector()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
WorldSession.EntitySpawn seed = Spawn(
|
|
0x7000000Au, 7, 20, 1, Position(0x0101FFFFu, 10f), 0x408u);
|
|
seed = seed with
|
|
{
|
|
Physics = seed.Physics!.Value with { Velocity = new Vector3(4f, 5f, 6f) },
|
|
};
|
|
controller.AcceptCreate(seed);
|
|
|
|
Assert.True(controller.TryApplyPosition(
|
|
new WorldSession.EntityPositionUpdate(
|
|
seed.Guid,
|
|
Position(0x0101FFFFu, 20f),
|
|
null,
|
|
null,
|
|
true,
|
|
7,
|
|
21,
|
|
0,
|
|
0),
|
|
false,
|
|
null,
|
|
null,
|
|
out PositionTimestampDisposition disposition,
|
|
out WorldSession.EntitySpawn accepted,
|
|
out _));
|
|
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
Assert.Equal(Vector3.Zero, accepted.Physics!.Value.Velocity);
|
|
}
|
|
|
|
[Fact]
|
|
public void LocalNormalCorrectionRetainsLiveVelocity_ButFreshTeleportZerosIt()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
WorldSession.EntitySpawn seed = WithTimestamps(
|
|
Spawn(0x5000000Au, 7, 20, 1, Position(0x0101FFFFu, 10f), 0x408u),
|
|
teleport: 10);
|
|
controller.AcceptCreate(seed);
|
|
Vector3 liveVelocity = new(4f, 5f, 6f);
|
|
|
|
Assert.True(controller.TryApplyPosition(
|
|
new WorldSession.EntityPositionUpdate(
|
|
seed.Guid,
|
|
Position(0x0101FFFFu, 20f),
|
|
new Vector3(90f, 80f, 70f),
|
|
null,
|
|
true,
|
|
7,
|
|
21,
|
|
10,
|
|
0),
|
|
true,
|
|
null,
|
|
liveVelocity,
|
|
out PositionTimestampDisposition normalDisposition,
|
|
out WorldSession.EntitySpawn normal,
|
|
out AcceptedPhysicsTimestamps normalTimestamps));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, normalDisposition);
|
|
Assert.Equal(liveVelocity, normal.Physics!.Value.Velocity);
|
|
Assert.False(normalTimestamps.TeleportAdvanced);
|
|
|
|
Assert.True(controller.TryApplyPosition(
|
|
new WorldSession.EntityPositionUpdate(
|
|
seed.Guid,
|
|
Position(0x0101FFFFu, 30f),
|
|
new Vector3(9f, 8f, 7f),
|
|
null,
|
|
true,
|
|
7,
|
|
22,
|
|
11,
|
|
0),
|
|
true,
|
|
null,
|
|
liveVelocity,
|
|
out PositionTimestampDisposition teleportDisposition,
|
|
out WorldSession.EntitySpawn teleported,
|
|
out AcceptedPhysicsTimestamps teleportTimestamps));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, teleportDisposition);
|
|
Assert.Equal(Vector3.Zero, teleported.Physics!.Value.Velocity);
|
|
Assert.True(teleportTimestamps.TeleportAdvanced);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParentEventWaitsForParent_ThenConsumesChildPositionOnce()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
controller.AcceptCreate(Spawn(
|
|
0x70000003u, 3, 4, 1, Position(0x0101FFFFu, 10f), 0x408u));
|
|
var update = new ParentEvent.Parsed(
|
|
0x70000004u, 0x70000003u, 1, 2, 9, 5);
|
|
|
|
Assert.False(controller.TryApplyParent(update, out _));
|
|
controller.AcceptCreate(Spawn(
|
|
0x70000004u, 9, 1, 1, Position(0x0101FFFFu, 15f), 0x408u));
|
|
Assert.True(controller.TryApplyParent(update, out WorldSession.EntitySpawn attached));
|
|
Assert.Equal(0x70000004u, attached.ParentGuid);
|
|
Assert.Null(attached.Position);
|
|
Assert.False(controller.TryApplyParent(update, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void LocalDeleteIsRejected_AndClearDropsSessionState()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
controller.AcceptCreate(Spawn(
|
|
0x50000001u, 4, 1, 1, Position(0x0101FFFFu, 10f), 0x408u));
|
|
|
|
Assert.False(controller.TryDelete(new DeleteObject.Parsed(0x50000001u, 4), true));
|
|
Assert.True(controller.TryGetSnapshot(0x50000001u, out _));
|
|
|
|
controller.Clear();
|
|
Assert.Empty(controller.Snapshots);
|
|
Assert.False(controller.IsFreshTeleportStart(0x50000001u, 1));
|
|
}
|
|
|
|
[Fact]
|
|
public void DeleteForUnknownGenerationIsRejected()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
|
|
Assert.False(controller.TryDelete(
|
|
new DeleteObject.Parsed(0x5000FFFFu, 9), isLocalPlayer: false));
|
|
}
|
|
|
|
[Fact]
|
|
public void TeleportStartIsComparedWithoutAdvancingTeleportTimestamp()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
WorldSession.EntitySpawn spawn = Spawn(
|
|
0x50000002u, 2, 1, 1, Position(0x0101FFFFu, 10f), 0x408u);
|
|
PhysicsSpawnData physics = spawn.Physics!.Value with
|
|
{
|
|
Timestamps = spawn.Physics.Value.Timestamps with { Teleport = 10 },
|
|
};
|
|
controller.AcceptCreate(spawn with { Physics = physics });
|
|
|
|
Assert.True(controller.IsFreshTeleportStart(0x50000002u, 10));
|
|
Assert.False(controller.IsFreshTeleportStart(0x50000002u, 9));
|
|
Assert.True(controller.IsFreshTeleportStart(0x50000002u, 11));
|
|
Assert.True(controller.IsFreshTeleportStart(0x50000002u, 11));
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectedServerControlStillMirrorsConsumedMovementTimestamp()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
WorldSession.EntitySpawn spawn = WithTimestamps(
|
|
Spawn(0x70000005u, 3, 1, 1, Position(0x0101FFFFu, 10f), 0x408u),
|
|
movement: 1,
|
|
serverControl: 5);
|
|
controller.AcceptCreate(spawn);
|
|
|
|
bool applied = controller.TryApplyMotion(
|
|
new WorldSession.EntityMotionUpdate(
|
|
spawn.Guid,
|
|
new CreateObject.ServerMotionState(0x3d, 0x11),
|
|
3,
|
|
2,
|
|
4,
|
|
false),
|
|
retainPayload: true,
|
|
out _,
|
|
out _);
|
|
|
|
Assert.False(applied);
|
|
Assert.True(controller.TryGetSnapshot(spawn.Guid, out WorldSession.EntitySpawn retained));
|
|
Assert.Equal((ushort)2, retained.MovementSequence);
|
|
Assert.Equal((ushort)5, retained.ServerControlSequence);
|
|
Assert.Equal((ushort)2, retained.Physics!.Value.Timestamps.Movement);
|
|
Assert.Equal((ushort)5, retained.Physics.Value.Timestamps.ServerControlledMove);
|
|
Assert.Equal(spawn.MotionState, retained.MotionState);
|
|
}
|
|
|
|
[Fact]
|
|
public void AutonomousLocalEchoRetainsPayloadButMirrorsAcceptedTimestamps()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
WorldSession.EntitySpawn spawn = WithTimestamps(
|
|
Spawn(0x50000006u, 3, 1, 1, Position(0x0101FFFFu, 10f), 0x408u),
|
|
movement: 1,
|
|
serverControl: 5) with
|
|
{
|
|
MotionState = new CreateObject.ServerMotionState(0x3d, 0x10),
|
|
};
|
|
controller.AcceptCreate(spawn);
|
|
|
|
Assert.True(controller.TryApplyMotion(
|
|
new WorldSession.EntityMotionUpdate(
|
|
spawn.Guid,
|
|
new CreateObject.ServerMotionState(0x3d, 0x12),
|
|
3,
|
|
2,
|
|
5,
|
|
true),
|
|
retainPayload: false,
|
|
out WorldSession.EntitySpawn retained,
|
|
out _));
|
|
|
|
Assert.Equal(spawn.MotionState, retained.MotionState);
|
|
Assert.Equal((ushort)2, retained.MovementSequence);
|
|
Assert.Equal((ushort)5, retained.ServerControlSequence);
|
|
Assert.Equal((ushort)2, retained.Physics!.Value.Timestamps.Movement);
|
|
}
|
|
|
|
[Fact]
|
|
public void FreshForceWithOlderTeleportMirrorsForceButRejectsPose()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
WorldSession.EntitySpawn spawn = WithTimestamps(
|
|
Spawn(0x50000007u, 3, 10, 1, Position(0x0101FFFFu, 10f), 0x408u),
|
|
teleport: 10,
|
|
forcePosition: 0);
|
|
controller.AcceptCreate(spawn);
|
|
|
|
Assert.True(controller.TryApplyPosition(
|
|
new WorldSession.EntityPositionUpdate(
|
|
spawn.Guid,
|
|
Position(0x0101FFFFu, 99f),
|
|
null,
|
|
null,
|
|
true,
|
|
3,
|
|
11,
|
|
9,
|
|
1),
|
|
isLocalPlayer: true,
|
|
forcePositionRotation: Quaternion.Identity,
|
|
currentLocalVelocity: Vector3.Zero,
|
|
out PositionTimestampDisposition disposition,
|
|
out WorldSession.EntitySpawn retained,
|
|
out _));
|
|
|
|
Assert.Equal(PositionTimestampDisposition.Rejected, disposition);
|
|
Assert.Equal(10f, retained.Position!.Value.PositionX);
|
|
Assert.Equal((ushort)10, retained.PositionSequence);
|
|
Assert.Equal((ushort)1, retained.Physics!.Value.Timestamps.ForcePosition);
|
|
Assert.Equal((ushort)10, retained.Physics.Value.Timestamps.Position);
|
|
}
|
|
|
|
[Fact]
|
|
public void ForcePositionStoresPreservedLocalHeadingForLaterHydration()
|
|
{
|
|
var controller = new InboundPhysicsStateController();
|
|
WorldSession.EntitySpawn spawn = WithTimestamps(
|
|
Spawn(0x50000008u, 3, 10, 1, Position(0x0101FFFFu, 10f), 0x408u),
|
|
teleport: 10,
|
|
forcePosition: 0);
|
|
controller.AcceptCreate(spawn);
|
|
Quaternion heading = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f);
|
|
Vector3 liveVelocity = new(1f, 2f, 3f);
|
|
|
|
Assert.True(controller.TryApplyPosition(
|
|
new WorldSession.EntityPositionUpdate(
|
|
spawn.Guid,
|
|
Position(0x0101FFFFu, 99f),
|
|
new Vector3(90f, 80f, 70f),
|
|
null,
|
|
true,
|
|
3,
|
|
9,
|
|
10,
|
|
1),
|
|
isLocalPlayer: true,
|
|
forcePositionRotation: heading,
|
|
currentLocalVelocity: liveVelocity,
|
|
out PositionTimestampDisposition disposition,
|
|
out WorldSession.EntitySpawn retained,
|
|
out _));
|
|
|
|
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
|
Assert.Equal(99f, retained.Position!.Value.PositionX);
|
|
Assert.Equal(heading.W, retained.Position.Value.RotationW, precision: 6);
|
|
Assert.Equal(heading.X, retained.Position.Value.RotationX, precision: 6);
|
|
Assert.Equal(heading.Y, retained.Position.Value.RotationY, precision: 6);
|
|
Assert.Equal(heading.Z, retained.Position.Value.RotationZ, precision: 6);
|
|
Assert.Equal(retained.Position, retained.Physics!.Value.Position);
|
|
Assert.Equal(liveVelocity, retained.Physics.Value.Velocity);
|
|
}
|
|
|
|
private static WorldSession.EntitySpawn WithTimestamps(
|
|
WorldSession.EntitySpawn spawn,
|
|
ushort? movement = null,
|
|
ushort? serverControl = null,
|
|
ushort? teleport = null,
|
|
ushort? forcePosition = null)
|
|
{
|
|
PhysicsSpawnData physics = spawn.Physics!.Value;
|
|
PhysicsTimestamps timestamps = physics.Timestamps with
|
|
{
|
|
Movement = movement ?? physics.Timestamps.Movement,
|
|
ServerControlledMove = serverControl ?? physics.Timestamps.ServerControlledMove,
|
|
Teleport = teleport ?? physics.Timestamps.Teleport,
|
|
ForcePosition = forcePosition ?? physics.Timestamps.ForcePosition,
|
|
};
|
|
return spawn with
|
|
{
|
|
MovementSequence = timestamps.Movement,
|
|
ServerControlSequence = timestamps.ServerControlledMove,
|
|
Physics = physics with { Timestamps = timestamps },
|
|
};
|
|
}
|
|
|
|
private static WorldSession.EntitySpawn Spawn(
|
|
uint guid,
|
|
ushort instance,
|
|
ushort positionSequence,
|
|
ushort stateSequence,
|
|
CreateObject.ServerPosition? position,
|
|
uint state)
|
|
{
|
|
var timestamps = new PhysicsTimestamps(
|
|
positionSequence,
|
|
1,
|
|
stateSequence,
|
|
1,
|
|
0,
|
|
1,
|
|
0,
|
|
1,
|
|
instance);
|
|
var physics = new PhysicsSpawnData(
|
|
RawState: state,
|
|
Position: position,
|
|
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,
|
|
position,
|
|
0x02000001u,
|
|
Array.Empty<CreateObject.AnimPartChange>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
null,
|
|
null,
|
|
"fixture",
|
|
null,
|
|
null,
|
|
0x09000001u,
|
|
PhysicsState: state,
|
|
InstanceSequence: instance,
|
|
MovementSequence: 1,
|
|
ServerControlSequence: 1,
|
|
PositionSequence: positionSequence,
|
|
Physics: physics);
|
|
}
|
|
|
|
private static CreateObject.ServerPosition Position(uint cell, float x) =>
|
|
new(cell, x, 10f, 5f, 1f, 0f, 0f, 0f);
|
|
}
|