acdream/tests/AcDream.Runtime.Tests/Entities/InboundPhysicsStateControllerTests.cs
Erik 19d9509497 fix(physics): #307 — PreviousTeleport was always 0 on the live Position path
Shipped defect in route 2 (9966b531), found while reviewing route 4a.

`InboundPhysicsStateController.TryApplyPosition` built its AcceptedPhysicsTimestamps
via `Current(gate, teleportAdvanced: ...)`, omitting `previousTeleport`, which
defaulted to a literal 0. The only site that populated it was the deferred
initial-create path — which is why the continuation executor was correct and
every newer consumer was not.

Consequence in shipped code: route 2 feeds this into
`ValidAcceptedAuthority`, which requires Previous == Accepted for a
ForcePosition. Any local player whose TELEPORT_TS is nonzero — anyone who has
portalled or recalled this session — had the authority rejected and the force
correction SILENTLY DROPPED. The user's @pklite acceptance was genuine but
narrow: that character had not teleported, so the stamp was still 0.

Second latent consequence: with an accepted stamp >= 0x8000, wrap-safe
TeleportRegressed also fires against the 0 and rejects ordinary Apply positions,
not just ForcePosition.

The fix captures `previousTeleport = gate.TeleportTimestamp` BEFORE
`TryAcceptPositionEvent` mutates it, matching the shape the deferred path
already used. Ordering is the whole point: capturing after would make
Previous == Accepted unconditionally, so ValidAcceptedAuthority's check would
pass vacuously — the symptom would disappear while the semantics broke.

Also removes the footgun that allowed it. `Current`'s parameter is now
`ushort? previousTeleport = null` resolving to `gate.TeleportTimestamp`, so the
eleven non-Position channels — none of which can move TELEPORT_TS — get
"previous == current" by omission rather than a literal 0 that is
indistinguishable from a genuine "never teleported".

Consumer audit: only TryApplyPosition was defective. The two route-2 call sites
trace back to it; the RuntimeEntityObjectLifetime sites source from
TryAcceptDeferredPosition and were already correct.

Tests discrimination-verified by reverting the argument to 0: the stamp test
fails Expected 10 / Actual 0, and the classifier test fails Expected
SetPositionSimple / Actual RejectedAuthority — the shipped defect reproduced
exactly.

Gates: complete Release solution 10,935 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:48:09 +02:00

710 lines
27 KiB
C#

using System.Numerics;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Tests.Entities;
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_ThenStagesTimestampUntilValidatedCommit()
{
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 staged));
Assert.Null(staged.ParentGuid);
Assert.NotNull(staged.Position);
Assert.Equal((ushort)5, staged.PositionSequence);
Assert.True(controller.TryCommitParent(
update.ChildGuid,
update.ParentGuid,
update.ParentLocation,
update.PlacementId,
update.ChildPositionSequence,
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 StaleMovementTimestampLeavesLegacySnapshotByteIdentical()
{
var controller = new InboundPhysicsStateController();
WorldSession.EntitySpawn spawn = WithTimestamps(
Spawn(0x70000008u, 3, 1, 1, Position(0x0101FFFFu, 10f), 0x408u),
movement: 5,
serverControl: 5);
controller.AcceptCreate(spawn);
Assert.True(controller.TryGetSnapshot(spawn.Guid, out WorldSession.EntitySpawn before));
// MOVEMENT_TS 3 is OLDER than the stored 5:
// PhysicsTimestampGate.TryAcceptMovementEvent's
// AdvanceStrict(Movement, ...) fails before ever consulting
// SERVER_CONTROLLED_MOVE_TS - the gate is entirely untouched, and
// the canonical snapshot must not move either (the regression this
// guards: ApplyAcceptedMotion stamping the WIRE's own stale
// MovementSequence instead of the gate's unchanged post-call value).
bool applied = controller.TryApplyMotion(
new WorldSession.EntityMotionUpdate(
spawn.Guid,
new CreateObject.ServerMotionState(0x3d, 0x99),
InstanceSequence: 3,
MovementSequence: 3,
ServerControlSequence: 9,
IsAutonomous: false),
retainPayload: true,
out WorldSession.EntitySpawn accepted,
out _);
Assert.False(applied);
Assert.Equal(default, accepted);
Assert.True(controller.TryGetSnapshot(spawn.Guid, out WorldSession.EntitySpawn after));
Assert.Equal(before, after);
}
[Fact]
public void InstanceMismatchedMovementLeavesLegacySnapshotByteIdentical()
{
var controller = new InboundPhysicsStateController();
WorldSession.EntitySpawn spawn = WithTimestamps(
Spawn(0x70000009u, 3, 1, 1, Position(0x0101FFFFu, 10f), 0x408u),
movement: 1,
serverControl: 1);
controller.AcceptCreate(spawn);
Assert.True(controller.TryGetSnapshot(spawn.Guid, out WorldSession.EntitySpawn before));
// A different INSTANCE_TS: TryAcceptInstance fails immediately,
// before MOVEMENT_TS or SERVER_CONTROLLED_MOVE_TS are ever
// consulted. Nothing in the gate or the canonical snapshot may move
// - a stale duplicate or foreign-incarnation packet must not
// silently drag a live entity's movement timestamps forward.
bool applied = controller.TryApplyMotion(
new WorldSession.EntityMotionUpdate(
spawn.Guid,
new CreateObject.ServerMotionState(0x3d, 0x99),
InstanceSequence: 4,
MovementSequence: 9,
ServerControlSequence: 9,
IsAutonomous: false),
retainPayload: true,
out WorldSession.EntitySpawn accepted,
out _);
Assert.False(applied);
Assert.Equal(default, accepted);
Assert.True(controller.TryGetSnapshot(spawn.Guid, out WorldSession.EntitySpawn after));
Assert.Equal(before, after);
}
[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);
}
/// <summary>
/// #307: the live Position path must report the TELEPORT_TS value the gate
/// held BEFORE the event, not 0. Every consumer of
/// <c>AcceptedPhysicsTimestamps.PreviousTeleport</c> compares it against
/// the accepted stamp; a hard 0 makes "this packet did not advance
/// TELEPORT_TS" indistinguishable from "this packet regressed it" for any
/// entity that has ever teleported.
/// </summary>
[Fact]
public void TryApplyPosition_ReportsThePreEventTeleportStamp()
{
var controller = new InboundPhysicsStateController();
WorldSession.EntitySpawn spawn = WithTimestamps(
Spawn(0x50000010u, 3, 10, 1, Position(0x0101FFFFu, 10f), 0x408u),
teleport: 10,
forcePosition: 0);
controller.AcceptCreate(spawn);
// An ordinary Position carrying the SAME TELEPORT_TS: previous and
// accepted must both be the live stamp, and nothing advanced.
Assert.True(controller.TryApplyPosition(
PositionUpdate(spawn.Guid, instance: 3, position: 11, teleport: 10),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out PositionTimestampDisposition steady,
out _,
out AcceptedPhysicsTimestamps steadyStamps));
Assert.Equal(PositionTimestampDisposition.Apply, steady);
Assert.Equal((ushort)10, steadyStamps.PreviousTeleport);
Assert.Equal((ushort)10, steadyStamps.Teleport);
Assert.False(steadyStamps.TeleportAdvanced);
// A fresh TELEPORT_TS: previous is the PRE-event stamp, accepted is
// the new one. Capturing after the gate mutated would collapse both
// onto 11 and silently lose the advance.
Assert.True(controller.TryApplyPosition(
PositionUpdate(spawn.Guid, instance: 3, position: 12, teleport: 11),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out PositionTimestampDisposition advanced,
out _,
out AcceptedPhysicsTimestamps advancedStamps));
Assert.Equal(PositionTimestampDisposition.Apply, advanced);
Assert.Equal((ushort)10, advancedStamps.PreviousTeleport);
Assert.Equal((ushort)11, advancedStamps.Teleport);
Assert.True(advancedStamps.TeleportAdvanced);
}
/// <summary>
/// #307, the shipped consequence: a local player who has portalled or
/// recalled this session holds a nonzero TELEPORT_TS. Retail's
/// FORCE_POSITION branch (<c>SmartBox::HandleReceivedPosition</c>
/// 0x00453FD0) fires only when the packet's teleport stamp EQUALS the live
/// one and never advances it, so the authority C4 route 2 builds from
/// these timestamps must satisfy
/// <c>PreviousTeleportSequence == AcceptedTeleportSequence</c>. With
/// PreviousTeleport pinned to 0 the classifier rejected the authority and
/// the force correction was silently dropped.
/// </summary>
[Fact]
public void LocalPlayerForcePositionAfterATeleport_ClassifiesAsAnAcceptedForceCorrection()
{
var controller = new InboundPhysicsStateController();
WorldSession.EntitySpawn spawn = WithTimestamps(
Spawn(0x50000011u, 3, 10, 1, Position(0x0101FFFFu, 10f), 0x408u),
teleport: 10,
forcePosition: 0);
controller.AcceptCreate(spawn);
Assert.True(controller.TryApplyPosition(
PositionUpdate(
spawn.Guid,
instance: 3,
position: 9,
teleport: 10,
forcePosition: 1),
isLocalPlayer: true,
forcePositionRotation: Quaternion.Identity,
currentLocalVelocity: Vector3.Zero,
out PositionTimestampDisposition disposition,
out WorldSession.EntitySpawn accepted,
out AcceptedPhysicsTimestamps timestamps));
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
// Built exactly as the route-2 drive builds it from these outputs.
var authority = new RuntimeAuthoritativePositionAuthority(
new RuntimeGenerationToken(7),
new RuntimeEntityKey(spawn.Guid, 1),
PositionAuthorityVersion: 4UL,
AcceptedPositionSequence: 9,
timestamps.PreviousTeleport,
timestamps.Teleport,
disposition);
RuntimeAuthoritativePositionRoute route =
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition(
new RuntimeAcceptedPositionRouteRequest(
authority,
RuntimePositionEntityKind.LocalPlayer,
RuntimeAcceptedPositionSource.PositionEvent,
accepted.Position!.Value,
PlacementFrame: 0u,
PositionPackVelocity: Vector3.Zero,
CommittedCellId: 0x0101FFFFu,
HasContact: true,
PlayerDistance: 0f,
UsePositionFromServer: true,
HasAnimations: false,
default));
Assert.Equal(
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
route.Disposition);
Assert.True(route.SendPositionImmediately);
Assert.Equal((ushort)10, timestamps.PreviousTeleport);
Assert.Equal((ushort)10, timestamps.Teleport);
}
private static WorldSession.EntityPositionUpdate PositionUpdate(
uint guid,
ushort instance,
ushort position,
ushort teleport,
ushort forcePosition = 0) =>
new(
guid,
Position(0x0101FFFFu, 20f),
null,
null,
true,
instance,
position,
teleport,
forcePosition);
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);
}