Parse the complete PhysicsDesc plus F754/F755 packets, correct every PhysicsState bit, and gate all nine retail update channels with generation-safe immutable snapshots. Preserve ForcePosition, teleport, placement, velocity, parent, pickup, delete, and same-generation CreateObject ordering from the named client. Separate accepted logical lifecycle notifications from retained UI qualities, make GUID replacement and session reset clear every projection exactly once, and add packet, wraparound, malformed-input, parent FIFO, canonical-position, reconnect, and GUID-reuse conformance coverage. Co-Authored-By: Codex <noreply@openai.com>
490 lines
18 KiB
C#
490 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 _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, normalDisposition);
|
|
Assert.Equal(liveVelocity, normal.Physics!.Value.Velocity);
|
|
|
|
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 _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, teleportDisposition);
|
|
Assert.Equal(Vector3.Zero, teleported.Physics!.Value.Velocity);
|
|
}
|
|
|
|
[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);
|
|
}
|