refactor(world): extract live entity network updates

Move Position, Vector, State, Movement, and equal-generation CreateObject routing out of GameWindow while preserving per-channel authority, ForcePosition acknowledgement, and motion-runtime ownership. Add adversarial authority and exact-wire coverage so reentrant updates and GUID reuse cannot publish stale state.
This commit is contained in:
Erik 2026-07-21 19:11:49 +02:00
parent c203e4799a
commit aa90c64666
19 changed files with 3701 additions and 2241 deletions

View file

@ -0,0 +1,114 @@
using System.Buffers.Binary;
using System.Net;
using System.Numerics;
using AcDream.App.Input;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
namespace AcDream.App.Tests.Input;
public sealed class LocalPlayerImmediatePositionTests
{
[Fact]
public void ForcePositionAcknowledgement_SendsOneExactAutonomousPositionAndStampsIt()
{
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 9000));
session.PublishAcceptedLocalPhysicsTimestamps(
instance: 11,
serverControlledMove: 12,
teleport: 13,
forcePosition: 14);
var sent = new List<byte[]>();
session.GameActionCapture = sent.Add;
PlayerMovementController player = GroundedPlayer();
Position preTurnCanonical = player.CellPosition;
Quaternion rotation = Quaternion.Normalize(
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.7f));
player.SetBodyOrientation(rotation);
Assert.NotEqual(rotation, preTurnCanonical.Frame.Orientation);
Assert.True(player.TryGetOutboundPosition(out Position outboundPosition));
var outbound = new LocalPlayerOutboundController((_, _, _, _, _, _) => { });
outbound.SendImmediatePosition(session, player);
byte[] body = Assert.Single(sent);
Assert.Equal(56, body.Length);
Assert.Equal(AutonomousPosition.GameActionOpcode, U32(body, 0));
Assert.Equal(1u, U32(body, 4));
Assert.Equal(AutonomousPosition.AutonomousPositionAction, U32(body, 8));
Assert.Equal(outboundPosition.ObjCellId, U32(body, 12));
Assert.Equal(outboundPosition.Frame.Origin.X, F32(body, 16));
Assert.Equal(outboundPosition.Frame.Origin.Y, F32(body, 20));
Assert.Equal(outboundPosition.Frame.Origin.Z, F32(body, 24));
Assert.Equal(rotation.W, F32(body, 28), precision: 6);
Assert.Equal(rotation.X, F32(body, 32), precision: 6);
Assert.Equal(rotation.Y, F32(body, 36), precision: 6);
Assert.Equal(rotation.Z, F32(body, 40), precision: 6);
Assert.Equal((ushort)11, U16(body, 44));
Assert.Equal((ushort)12, U16(body, 46));
Assert.Equal((ushort)13, U16(body, 48));
Assert.Equal((ushort)14, U16(body, 50));
Assert.Equal((byte)1, body[52]);
Assert.True(player.TryGetOutboundPosition(out Position currentOutbound));
Assert.False(player.ShouldSendPositionEvent(
currentOutbound,
player.ContactPlane,
player.SimTimeSeconds));
}
[Fact]
public void ImmediateAcknowledgement_RequiresSessionPlayerAndGroundContact()
{
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 9000));
var sent = new List<byte[]>();
session.GameActionCapture = sent.Add;
var outbound = new LocalPlayerOutboundController((_, _, _, _, _, _) => { });
PlayerMovementController grounded = GroundedPlayer();
outbound.SendImmediatePosition(null, grounded);
outbound.SendImmediatePosition(session, null);
outbound.SendImmediatePosition(
session,
new PlayerMovementController(new PhysicsEngine()));
Assert.Empty(sent);
}
private static PlayerMovementController GroundedPlayer()
{
var engine = new PhysicsEngine();
var heights = new byte[81];
Array.Fill(heights, (byte)50);
var heightTable = new float[256];
for (int i = 0; i < heightTable.Length; i++)
heightTable[i] = i;
engine.AddLandblock(
0xA9B4FFFFu,
new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
var player = new PlayerMovementController(engine);
player.SetPosition(
new Vector3(96f, 97f, 50f),
0xA9B40001u,
new Vector3(96f, 97f, 50f));
Assert.True(player.CanSendPositionEvent);
return player;
}
private static uint U32(byte[] body, int offset) =>
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(offset, 4));
private static ushort U16(byte[] body, int offset) =>
BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(offset, 2));
private static float F32(byte[] body, int offset) =>
BitConverter.Int32BitsToSingle(
BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(offset, 4)));
}

View file

@ -0,0 +1,85 @@
using System.Numerics;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Physics.Motion;
using AcDream.Core.World;
namespace AcDream.App.Tests.Physics;
public sealed class DeferredLiveEntityMotionRuntimeBindingsTests
{
[Fact]
public void EveryReverseEdgeFailsBeforeBind()
{
var bridge = new DeferredLiveEntityMotionRuntimeBindings();
var entity = Entity();
Assert.Throws<InvalidOperationException>(() =>
bridge.GetSetupCylinder(1, entity));
Assert.Throws<InvalidOperationException>(() =>
bridge.RouteServerMoveTo(null!, 0, default));
Assert.Throws<InvalidOperationException>(() =>
bridge.StickToObjectFromWire(null, 1));
Assert.Throws<InvalidOperationException>(() =>
bridge.ClearTargetForHiddenEntity(1));
Assert.Throws<InvalidOperationException>(() =>
bridge.ResolvePhysicsHost(1));
}
[Fact]
public void BindIsSingleAssignmentAndAllCallsReachExactOwner()
{
var bridge = new DeferredLiveEntityMotionRuntimeBindings();
var owner = new RecordingBindings();
bridge.Bind(owner);
Assert.Equal((2f, 3f), bridge.GetSetupCylinder(1, Entity()));
Assert.True(bridge.RouteServerMoveTo(null!, 4, default));
bridge.StickToObjectFromWire(null, 5);
bridge.ClearTargetForHiddenEntity(6);
Assert.Null(bridge.ResolvePhysicsHost(7));
Assert.Equal(
["cylinder:1", "moveto:4", "stick:5", "hidden:6", "host:7"],
owner.Calls);
Assert.Throws<InvalidOperationException>(() =>
bridge.Bind(new RecordingBindings()));
}
private static WorldEntity Entity() => new()
{
Id = 1,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
private sealed class RecordingBindings : ILiveEntityMotionRuntimeBindings
{
public List<string> Calls { get; } = [];
public (float Radius, float Height) GetSetupCylinder(uint guid, WorldEntity entity)
{
Calls.Add($"cylinder:{guid}");
return (2f, 3f);
}
public bool RouteServerMoveTo(
MovementManager movement,
uint cellId,
WorldSession.EntityMotionUpdate update)
{
Calls.Add($"moveto:{cellId}");
return true;
}
public void StickToObjectFromWire(IPhysicsObjHost? host, uint targetGuid) =>
Calls.Add($"stick:{targetGuid}");
public void ClearTargetForHiddenEntity(uint guid) => Calls.Add($"hidden:{guid}");
public IPhysicsObjHost? ResolvePhysicsHost(uint guid)
{
Calls.Add($"host:{guid}");
return null;
}
}
}

View file

@ -0,0 +1,408 @@
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;
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);
}
}

View file

@ -0,0 +1,96 @@
using AcDream.App.Physics;
namespace AcDream.App.Tests.Physics;
public sealed class LiveEntityNetworkBranchRoutingTests
{
[Fact]
public void Vector_ProjectileStopsCanonicalAndOrdinaryRoutes()
{
var calls = new List<string>();
LiveEntityVectorRoute route = LiveEntityVectorRouter.Route(
() => { calls.Add("projectile"); return true; },
() => { calls.Add("canonical"); return true; },
() => calls.Add("ordinary"));
Assert.Equal(LiveEntityVectorRoute.Projectile, route);
Assert.Equal(["projectile"], calls);
}
[Fact]
public void Vector_CanonicalBodyRunsOnlyAfterProjectileDeclines()
{
var calls = new List<string>();
LiveEntityVectorRoute route = LiveEntityVectorRouter.Route(
() => { calls.Add("projectile"); return false; },
() => { calls.Add("canonical"); return true; },
() => calls.Add("ordinary"));
Assert.Equal(LiveEntityVectorRoute.CanonicalBody, route);
Assert.Equal(["projectile", "canonical"], calls);
}
[Fact]
public void Vector_OrdinaryRemoteIsTheLastFallback()
{
var calls = new List<string>();
LiveEntityVectorRoute route = LiveEntityVectorRouter.Route(
() => { calls.Add("projectile"); return false; },
() => { calls.Add("canonical"); return false; },
() => calls.Add("ordinary"));
Assert.Equal(LiveEntityVectorRoute.OrdinaryRemote, route);
Assert.Equal(["projectile", "canonical", "ordinary"], calls);
}
[Fact]
public void ForcePosition_BlipsAndAcknowledgesExactlyOnce()
{
int currentChecks = 0;
int blips = 0;
int acknowledgements = 0;
bool completed = LocalForcePositionTransaction.Apply(
isForcePosition: true,
() => { currentChecks++; return true; },
() => blips++,
() => acknowledgements++);
Assert.True(completed);
Assert.Equal(2, currentChecks);
Assert.Equal(1, blips);
Assert.Equal(1, acknowledgements);
}
[Fact]
public void ForcePosition_AcknowledgementInvalidationStopsTheTail()
{
bool current = true;
int acknowledgements = 0;
bool completed = LocalForcePositionTransaction.Apply(
isForcePosition: true,
() => current,
() => { },
() => { acknowledgements++; current = false; });
Assert.False(completed);
Assert.Equal(1, acknowledgements);
}
[Fact]
public void OrdinaryPositionDoesNotBlipOrAcknowledge()
{
int calls = 0;
Assert.True(LocalForcePositionTransaction.Apply(
isForcePosition: false,
() => { calls++; return false; },
() => calls++,
() => calls++));
Assert.Equal(0, calls);
}
}

View file

@ -30,6 +30,23 @@ public sealed class GameWindowLiveEntityCompositionTests
[InlineData("OnLiveEntityPruned")]
[InlineData("TearDownLiveEntityRuntimeComponents")]
[InlineData("CreateLiveEntityRuntimeTeardownPlan")]
[InlineData("RouteSameGenerationCreateObject")]
[InlineData("OnLiveMotionUpdated")]
[InlineData("OnLivePositionUpdated")]
[InlineData("OnLiveVectorUpdated")]
[InlineData("OnLiveStateUpdated")]
[InlineData("EnsureRemoteMotionBindings")]
[InlineData("ResolvePhysicsHost")]
[InlineData("GetSetupCylinder")]
[InlineData("RouteServerMoveTo")]
[InlineData("StickToObjectFromWire")]
[InlineData("ClearTargetForHiddenEntity")]
[InlineData("ApplyServerControlledVelocityCycle")]
[InlineData("RunRemoteTeleportHook")]
[InlineData("SendImmediateLocalPositionEvent")]
[InlineData("DispatchRemoteInboundMotion")]
[InlineData("CreateRemoteMotion")]
[InlineData("WillAdvanceRemoteMotion")]
public void GameWindow_DoesNotReacquireExtractedLiveEntityBodies(string methodName)
{
Assert.Null(typeof(GameWindow).GetMethod(methodName, PrivateImplementation));
@ -52,6 +69,10 @@ public sealed class GameWindowLiveEntityCompositionTests
[InlineData(typeof(LiveEntityHydrationController))]
[InlineData(typeof(DatLiveEntityProjectionMaterializer))]
[InlineData(typeof(LiveEntityRuntimeTeardownController))]
[InlineData(typeof(LiveEntityNetworkUpdateController))]
[InlineData(typeof(LiveEntityMotionRuntimeController))]
[InlineData(typeof(LiveEntityInboundAuthorityGate))]
[InlineData(typeof(DeferredLiveEntityMotionRuntimeBindings))]
public void ExtractedHelpers_DoNotOwnGuidIndexesOrBackendState(Type helperType)
{
foreach (FieldInfo field in helperType.GetFields(

View file

@ -0,0 +1,94 @@
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 LiveEntitySameGenerationUpdateRouterTests
{
[Theory]
[InlineData(0, "parent")]
[InlineData(1, "position")]
[InlineData(2, "pickup")]
[InlineData(3, null)]
public void Apply_PreservesRetailTailOrderAndExclusiveSpatialBranch(
int spatialKind,
string? expectedSpatial)
{
var sink = new RecordingSink();
SameGenerationCreateObjectEvents refresh = new(
Description: default,
Appearance: default,
Parent: spatialKind == 0 ? default(CreateParentUpdate?) : null,
Position: spatialKind == 1
? default(WorldSession.EntityPositionUpdate?)
: null,
Pickup: spatialKind == 2 ? default(PickupEvent.Parsed?) : null,
Movement: default(WorldSession.EntityMotionUpdate?),
State: default,
Vector: default);
// Nullable default(T?) has no value; explicitly wrap the selected
// zero-valued fixture so the branch itself remains observable.
refresh = refresh with
{
Parent = spatialKind == 0
? (CreateParentUpdate?)new CreateParentUpdate()
: null,
Position = spatialKind == 1
? (WorldSession.EntityPositionUpdate?)new WorldSession.EntityPositionUpdate()
: null,
Pickup = spatialKind == 2
? (PickupEvent.Parsed?)new PickupEvent.Parsed()
: null,
Movement = new WorldSession.EntityMotionUpdate(),
};
LiveEntitySameGenerationUpdateRouter.Apply(refresh, sink);
var expected = new List<string> { "description", "appearance" };
if (expectedSpatial is not null)
expected.Add(expectedSpatial);
expected.AddRange(["movement", "state", "vector"]);
Assert.Equal(expected, sink.Events);
}
[Fact]
public void ParentTakesPrecedenceWhenMalformedFixtureOffersAllSpatialForms()
{
var sink = new RecordingSink();
var refresh = new SameGenerationCreateObjectEvents(
default,
default,
new CreateParentUpdate(),
new WorldSession.EntityPositionUpdate(),
new PickupEvent.Parsed(),
null,
default,
default);
LiveEntitySameGenerationUpdateRouter.Apply(refresh, sink);
Assert.Equal(
["description", "appearance", "parent", "state", "vector"],
sink.Events);
}
private sealed class RecordingSink : ILiveEntitySameGenerationUpdateSink
{
public List<string> Events { get; } = [];
public void OnDescription(uint ownerGuid, PhysicsSpawnData description) =>
Events.Add("description");
public void OnAppearance(ObjDescEvent.Parsed appearance) =>
Events.Add("appearance");
public void OnParent(CreateParentUpdate parent) => Events.Add("parent");
public void OnPosition(WorldSession.EntityPositionUpdate position) =>
Events.Add("position");
public void OnPickup(PickupEvent.Parsed pickup) => Events.Add("pickup");
public void OnMovement(WorldSession.EntityMotionUpdate movement) =>
Events.Add("movement");
public void OnState(SetState.Parsed state) => Events.Add("state");
public void OnVector(VectorUpdate.Parsed vector) => Events.Add("vector");
}
}