refactor(runtime): move accepted entity wire state
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>
This commit is contained in:
parent
b0fecc5f65
commit
f7442d13e9
18 changed files with 20 additions and 6 deletions
|
|
@ -0,0 +1,502 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
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 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue