acdream/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs
Erik a51ebc66e9 feat(physics): integrate live projectile runtime
Attach the retail projectile driver to canonical LiveEntityRecord ownership, sharing one PhysicsBody and full-cell identity with RemoteMotion regardless of creation order. Apply timestamp-gated state, vector, and position corrections in place, preserve active ordinary-body behavior when Missile clears, and keep renderer, effects, shadows, and spatial buckets synchronized across loaded/pending transitions.

Validate malformed packets before canonical timestamp mutation, validate adopted bodies from their current frame rather than stale CreateObject data, serialize late Setup resolution with streaming DAT reads, and preserve classification across clock anomalies. Retain shadow registrations through temporary leave-world residence while logical teardown remains generation-scoped.

Add 31 App lifecycle/adversarial tests plus Core shadow suspension coverage, and synchronize the retail pseudocode, architecture, milestones, roadmap, and durable physics memory.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-14 12:51:42 +02:00

1228 lines
47 KiB
C#

using System.Numerics;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
namespace AcDream.App.Tests.Physics;
public sealed class ProjectileControllerTests
{
private const uint Guid = 0x7000A001u;
private const uint CellA = 0x01010001u;
private const uint CellB = 0x01020001u;
private static readonly PhysicsStateFlags MissileState =
PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Missile
| PhysicsStateFlags.AlignPath
| PhysicsStateFlags.PathClipped;
private sealed class RecordingResources : ILiveEntityResourceLifecycle
{
public int Registers { get; private set; }
public int Unregisters { get; private set; }
public void Register(WorldEntity entity) => Registers++;
public void Unregister(WorldEntity entity) => Unregisters++;
}
[Fact]
public void Tick_MovesCanonicalProjectionWithoutRecreatingResources()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
uint localId = entity.Id;
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 10.0));
fixture.Controller.Tick(10.1, liveCenterX: 1, liveCenterY: 1, playerWorldPosition: null);
Assert.Same(entity, record.WorldEntity);
Assert.Equal(localId, entity.Id);
Assert.True(entity.Position.X > 10f);
Assert.Equal(1, fixture.Resources.Registers);
Assert.Equal(0, fixture.Resources.Unregisters);
Assert.Equal(1, fixture.Controller.Count);
Assert.Same(record.ProjectileRuntime!.Body, record.PhysicsBody);
}
[Fact]
public void PendingDestinationAndLaterHydration_RetainIdentityAndProjectileOwner()
{
var fixture = new Fixture(loadInitialLandblock: true);
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 2.0));
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
Guid,
worldPosition: new Vector3(202f, 10f, 5f),
cellLocalPosition: new Vector3(10f, 10f, 5f),
orientation: Quaternion.Identity,
fullCellId: CellB,
currentTime: 2.1,
liveCenterX: 1,
liveCenterY: 1));
Assert.False(record.IsSpatiallyVisible);
Assert.True(record.IsSpatiallyProjected);
Assert.Same(entity, record.WorldEntity);
Assert.Same(runtime, record.ProjectileRuntime);
Vector3 pendingPosition = entity.Position;
fixture.Controller.Tick(
2.4,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: null);
Assert.Equal(pendingPosition, entity.Position);
Assert.False(runtime.Body.InWorld);
Assert.False(runtime.Body.IsActive);
fixture.Spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
fixture.Controller.Tick(
2.5,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: null);
Assert.True(record.IsSpatiallyVisible);
Assert.Same(entity, record.WorldEntity);
Assert.Same(runtime, record.ProjectileRuntime);
Assert.Equal(1, fixture.Resources.Registers);
Assert.Equal(pendingPosition, entity.Position);
fixture.Controller.Tick(
2.6,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: null);
Assert.True(entity.Position.X > pendingPosition.X);
}
[Fact]
public void LoadedLandblockCrossing_RebucketsSameProjection()
{
const uint startCell = 0xA9B40039u;
const uint destinationCell = 0xAAB40001u;
var fixture = new Fixture(loadInitialLandblock: false, BuildBoundaryEngine());
fixture.Spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu));
fixture.Spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu));
LiveEntityRecord record = fixture.Spawn(
instance: 1,
cellId: startCell,
worldPosition: new Vector3(191f, 10f, 50f),
cellLocalPosition: new Vector3(191f, 10f, 50f),
velocity: new Vector3(40f, 0f, 0f));
WorldEntity entity = record.WorldEntity!;
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(radius: 0.5f), 0.0));
fixture.Controller.Tick(
0.05,
liveCenterX: 0xA9,
liveCenterY: 0xB4,
playerWorldPosition: null);
Assert.Equal(destinationCell, record.FullCellId);
Assert.Equal(destinationCell, entity.ParentCellId);
Assert.Equal(193f, entity.Position.X, 3);
Assert.Same(entity, record.WorldEntity);
Assert.Equal(1, fixture.Resources.Registers);
}
[Fact]
public void PredictedCrossingIntoPendingLandblock_SuspendsInSameTick()
{
const uint startCell = 0xA9B40039u;
var fixture = new Fixture(loadInitialLandblock: false, BuildBoundaryEngine());
fixture.Spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu));
LiveEntityRecord record = fixture.Spawn(
instance: 1,
cellId: startCell,
worldPosition: new Vector3(191f, 10f, 50f),
cellLocalPosition: new Vector3(191f, 10f, 50f),
velocity: new Vector3(40f, 0f, 0f));
WorldEntity entity = record.WorldEntity!;
fixture.Engine.ShadowObjects.Register(
entity.Id,
entity.SourceGfxObjOrSetupId,
entity.Position,
entity.Rotation,
radius: 0.5f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0xA9B4FFFFu,
seedCellId: startCell,
isStatic: false);
Assert.True(fixture.Controller.TryBind(
record,
ProjectileSetup(radius: 0.5f),
0.0,
liveCenterX: 0xA9,
liveCenterY: 0xB4));
fixture.Controller.Tick(
0.05,
liveCenterX: 0xA9,
liveCenterY: 0xB4,
playerWorldPosition: null);
Assert.Equal(0xAAB40001u, record.FullCellId);
Assert.False(record.IsSpatiallyVisible);
Assert.True(record.IsSpatiallyProjected);
Assert.False(record.PhysicsBody!.InWorld);
Assert.False(record.PhysicsBody.IsActive);
Assert.Empty(fixture.Engine.ShadowObjects.GetObjectsInCell(startCell));
Assert.Empty(fixture.Engine.ShadowObjects.GetObjectsInCell(0xAAB40001u));
Assert.Same(entity, record.WorldEntity);
Assert.Equal(1, fixture.Controller.Count);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 0.06, 0xA9, 0xB4));
fixture.Spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu));
fixture.Controller.Tick(0.07, 0xA9, 0xB4, playerWorldPosition: null);
Assert.True(record.IsSpatiallyVisible);
Assert.True(record.PhysicsBody.InWorld);
Assert.False(record.PhysicsBody.IsActive);
Assert.Contains(
fixture.Engine.ShadowObjects.GetObjectsInCell(0xAAB40001u),
entry => entry.EntityId == entity.Id);
}
[Fact]
public void FreshVectorAndPositionCorrectionsMutateSameBody()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 3);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 4.0));
PhysicsBody body = record.PhysicsBody!;
Assert.True(fixture.Controller.ApplyAuthoritativeVector(
Guid,
new Vector3(7f, 8f, 9f),
new Vector3(0f, 0f, 2f),
currentTime: 4.5));
Assert.Equal(new Vector3(7f, 8f, 9f), body.Velocity);
Assert.Equal(new Vector3(0f, 0f, 2f), body.Omega);
var correction = new Vector3(30f, 31f, 32f);
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
Guid,
correction,
new Vector3(30f, 31f, 32f),
Quaternion.Identity,
CellA,
currentTime: 5.0,
liveCenterX: 1,
liveCenterY: 1));
Assert.Same(body, record.PhysicsBody);
Assert.Equal(correction, body.Position);
Assert.Equal(correction, record.WorldEntity!.Position);
Assert.Equal(new Vector3(7f, 8f, 9f), body.Velocity);
}
[Fact]
public void StaleVectorGateCannotOverwritePredictedBody()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 5, vectorSequence: 10);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
Vector3 original = body.Velocity;
var stale = new VectorUpdate.Parsed(
Guid,
new Vector3(99f, 0f, 0f),
Vector3.Zero,
InstanceSequence: 5,
VectorSequence: 9);
Assert.False(fixture.Live.TryApplyVector(stale, out _));
Assert.Equal(original, body.Velocity);
}
[Fact]
public void MissileStateRemovalAndReaddition_ReclassifiesWithoutReplacingEntity()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
Vector3 before = record.WorldEntity!.Position;
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
PhysicsBody body = runtime.Body;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid,
PhysicsStateFlags.ReportCollisions,
currentTime: 1.1,
liveCenterX: 1,
liveCenterY: 1));
fixture.Controller.Tick(
1.2,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: null);
Assert.Same(runtime, record.ProjectileRuntime);
Assert.Same(body, record.PhysicsBody);
Assert.True(body.IsActive);
Assert.Equal(1, fixture.Controller.Count);
Assert.True(record.WorldEntity.Position.X > before.X);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid,
MissileState,
currentTime: 1.3,
liveCenterX: 1,
liveCenterY: 1));
Assert.Same(runtime, record.ProjectileRuntime);
Assert.Same(body, record.PhysicsBody);
Assert.True(body.IsActive); // State preserves an already-active body.
Assert.Same(record.WorldEntity, fixture.Live.MaterializedWorldEntities[Guid]);
Assert.Equal(1, fixture.Resources.Registers);
}
[Fact]
public void StateReclassificationAfterPredictedCrossing_KeepsCurrentCellAndBody()
{
const uint startCell = 0xA9B40039u;
const uint destinationCell = 0xAAB40001u;
var fixture = new Fixture(loadInitialLandblock: false, BuildBoundaryEngine());
fixture.Spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu));
fixture.Spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu));
LiveEntityRecord record = fixture.Spawn(
instance: 1,
cellId: startCell,
worldPosition: new Vector3(191f, 10f, 50f),
cellLocalPosition: new Vector3(191f, 10f, 50f),
velocity: new Vector3(40f, 0f, 0f));
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(0.5f), 0.0));
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
fixture.Controller.Tick(0.05, 0xA9, 0xB4, playerWorldPosition: null);
Assert.Equal(destinationCell, record.FullCellId);
Assert.NotEqual(record.Snapshot.Position!.Value.LandblockId, record.FullCellId);
Assert.True(fixture.Live.TryApplyState(
new SetState.Parsed(
Guid,
(uint)PhysicsStateFlags.ReportCollisions,
InstanceSequence: 1,
StateSequence: 2),
out _));
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 0.06, 0xA9, 0xB4));
Assert.Equal(destinationCell, record.FullCellId);
Assert.True(fixture.Live.TryApplyState(
new SetState.Parsed(
Guid,
(uint)MissileState,
InstanceSequence: 1,
StateSequence: 3),
out _));
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 0.07, 0xA9, 0xB4));
Assert.Same(runtime, record.ProjectileRuntime);
Assert.Same(runtime.Body, record.PhysicsBody);
Assert.Equal(destinationCell, record.FullCellId);
Assert.Equal(destinationCell, record.WorldEntity!.ParentCellId);
}
[Fact]
public void FreshVector_ClampsImmediatelyAndRestartsInactiveClock()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
body.TransientState &= ~TransientStateFlags.Active;
body.LastUpdateTime = 1.0;
Assert.True(fixture.Controller.ApplyAuthoritativeVector(
Guid,
new Vector3(100f, 0f, 0f),
Vector3.Zero,
currentTime: 20.0));
Assert.Equal(50f, body.Velocity.Length(), 4);
Assert.Equal(20.0, body.LastUpdateTime, 8);
fixture.Controller.Tick(
20.1,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: null);
Assert.Equal(15f, record.WorldEntity!.Position.X, 3);
}
[Fact]
public void StateOnlyUpdate_DoesNotRestartStoppedProjectile()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
body.TransientState &= ~TransientStateFlags.Active;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid,
MissileState | PhysicsStateFlags.Inelastic,
currentTime: 2.0,
liveCenterX: 1,
liveCenterY: 1));
Assert.False(body.IsActive);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 2.1, 1, 1));
Assert.False(body.IsActive);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 2.2, 1, 1));
Assert.False(body.IsActive);
}
[Fact]
public void VectorAfterMissileRemoval_StillMutatesRetainedRetailBody()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 1.01, 1, 1));
Assert.True(fixture.Controller.ApplyAuthoritativeVector(
Guid,
new Vector3(100f, 0f, 0f),
new Vector3(0f, 0f, 3f),
1.02));
Assert.Same(body, record.PhysicsBody);
Assert.Equal(50f, body.Velocity.Length(), 4);
Assert.Equal(new Vector3(0f, 0f, 3f), body.Omega);
Vector3 before = body.Position;
fixture.Controller.Tick(1.1, 1, 1, playerWorldPosition: null);
Assert.True(body.Position.X > before.X);
Assert.Equal(body.Position, record.WorldEntity!.Position);
}
[Fact]
public void VectorAfterMissileRemoval_WithRemoteMotionDefersToOrdinaryFunnel()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
fixture.Live.SetRemoteMotionRuntime(Guid, new GameWindow.RemoteMotion(body));
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 2.0, 1, 1));
Vector3 before = body.Velocity;
Assert.False(fixture.Controller.ApplyAuthoritativeVector(
Guid,
new Vector3(0f, 0f, 12f),
new Vector3(0f, 0f, 2f),
3.0));
Assert.Equal(before, body.Velocity);
Assert.Same(body, record.RemoteMotionRuntime!.Body);
}
[Fact]
public void MissileReacquisition_AlignsClockWithoutStateOnlyMotion()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
Vector3 position = body.Position;
fixture.Live.SetRemoteMotionRuntime(Guid, new GameWindow.RemoteMotion(body));
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 2.0, 1, 1));
body.TransientState |= TransientStateFlags.Active;
body.LastUpdateTime = 9.5; // generic remote interval used another clock owner
body.set_velocity(new Vector3(40f, 0f, 0f));
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 10.0, 1, 1));
Assert.Equal(10.0, body.LastUpdateTime, 8);
Assert.True(body.IsActive);
fixture.Controller.Tick(10.1, 1, 1, playerWorldPosition: null);
Assert.Equal(position.X + 4f, body.Position.X, 3);
Assert.Equal(body.Position, record.WorldEntity!.Position);
}
[Fact]
public void MissileReacquisition_AppliesStateWithoutPoisoningClock()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 2.0, 1, 1));
PhysicsStateFlags stateBefore = body.State;
double clockBefore = body.LastUpdateTime;
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, double.NaN, 1, 1));
Assert.Equal(record.FinalPhysicsState, body.State);
Assert.NotEqual(stateBefore, body.State);
Assert.NotEqual(clockBefore, body.LastUpdateTime);
Assert.Equal(2.0, body.LastUpdateTime, 8);
Assert.True(double.IsFinite(body.LastUpdateTime));
Assert.True(body.IsActive);
}
[Fact]
public void Tick_PublishesMovedRootToEffectPoseSource()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
Assert.True(fixture.Poses.TryGetRootPose(entity.Id, out Matrix4x4 before));
fixture.Controller.Tick(
1.1,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: null);
Assert.True(fixture.Poses.TryGetRootPose(entity.Id, out Matrix4x4 after));
Assert.NotEqual(before.Translation, after.Translation);
Assert.Equal(entity.Position, after.Translation);
}
[Fact]
public void Tick_PartArrayUsesRetailNinetySixUnitActivationBoundary()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1, hasRenderablePart: true);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
fixture.Controller.Tick(
1.1,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: new Vector3(106f, 10f, 5f));
Assert.Equal(11f, record.WorldEntity!.Position.X, 3);
fixture.Controller.Tick(
1.2,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: new Vector3(107.1f, 10f, 5f));
Assert.Equal(11f, record.WorldEntity.Position.X, 3);
Assert.False(record.PhysicsBody!.IsActive);
}
[Fact]
public void Bind_UsesCanonicalOutdoorCellAndRehydrationCellTracksPrediction()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(
instance: 1,
cellId: CellA,
worldPosition: new Vector3(30f, 10f, 5f),
cellLocalPosition: new Vector3(30f, 10f, 5f));
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
Assert.Equal(record.PhysicsBody!.CellPosition.ObjCellId, record.FullCellId);
Assert.Equal(record.FullCellId, record.WorldEntity!.ParentCellId);
Assert.Equal(record.FullCellId, record.ProjectionCellId);
Assert.NotEqual(record.Snapshot.Position!.Value.LandblockId, record.FullCellId);
}
[Fact]
public void MalformedFreshUpdates_DoNotPoisonCanonicalBodyOrPose()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
Vector3 velocity = body.Velocity;
Vector3 position = body.Position;
Assert.True(fixture.Controller.ApplyAuthoritativeVector(
Guid,
new Vector3(float.NaN, 0f, 0f),
Vector3.Zero,
currentTime: 2.0));
Assert.Equal(velocity, body.Velocity);
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
Guid,
new Vector3(float.PositiveInfinity, 0f, 0f),
new Vector3(float.PositiveInfinity, 0f, 0f),
Quaternion.Identity,
CellA,
currentTime: 2.1,
liveCenterX: 1,
liveCenterY: 1));
Assert.Equal(position, body.Position);
Assert.Equal(position, record.WorldEntity!.Position);
}
[Fact]
public void MalformedPayloadGate_RejectsBeforeAuthoritativeSnapshotAdvances()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1, vectorSequence: 4);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
WorldSession.EntitySpawn before = record.Snapshot;
Assert.False(fixture.Controller.CanAcceptVectorPayload(
Guid,
new Vector3(float.NaN, 0f, 0f),
Vector3.Zero));
Assert.False(fixture.Controller.CanAcceptPositionPayload(
Guid,
new CreateObject.ServerPosition(
CellA,
float.PositiveInfinity,
0f,
0f,
1f,
0f,
0f,
0f),
velocity: null));
var malformedPosition = new WorldSession.EntityPositionUpdate(
Guid,
new CreateObject.ServerPosition(
CellA,
20f,
20f,
5f,
1f,
0f,
0f,
0f),
Velocity: new Vector3(float.NaN, 0f, 0f),
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 0,
ForcePositionSequence: 0);
Assert.False(fixture.Controller.CanAcceptPositionPayload(
Guid,
malformedPosition.Position,
malformedPosition.Velocity));
Assert.Equal(before, record.Snapshot);
Assert.Equal((ushort)1, record.Snapshot.Physics!.Value.Timestamps.Position);
Assert.Equal((ushort)4, record.Snapshot.Physics!.Value.Timestamps.Vector);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 2.0, 1, 1));
Assert.NotNull(record.ProjectileRuntime);
Assert.True(float.IsFinite(record.PhysicsBody!.Velocity.X));
Assert.True(float.IsFinite(record.PhysicsBody.Velocity.Y));
Assert.True(float.IsFinite(record.PhysicsBody.Velocity.Z));
}
[Fact]
public void StateUpdate_AlwaysMutatesExistingCanonicalBodyBeforeProjectileAcquisition()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new GameWindow.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
PhysicsStateFlags ordinaryState =
PhysicsStateFlags.Gravity | PhysicsStateFlags.IgnoreCollisions;
record.FinalPhysicsState = ordinaryState;
Assert.False(fixture.Controller.ApplyAuthoritativeState(
Guid, ordinaryState, 1.0, 1, 1));
Assert.Equal(ordinaryState, remote.Body.State);
Assert.Null(record.ProjectileRuntime);
fixture.ResolvedSetup = new Setup();
PhysicsStateFlags unsupportedMissile =
ordinaryState | PhysicsStateFlags.Missile;
record.FinalPhysicsState = unsupportedMissile;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, unsupportedMissile, 1.1, 1, 1));
Assert.Equal(unsupportedMissile, remote.Body.State);
Assert.Null(record.ProjectileRuntime);
Assert.Same(remote.Body, record.PhysicsBody);
}
[Fact]
public void FirstMissileStateWithInvalidLocalClock_RetainsClassificationAndBindsOnce()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, double.NaN, 1, 1));
Assert.NotNull(record.ProjectileRuntime);
Assert.Equal(MissileState, record.PhysicsBody!.State);
Assert.True(double.IsFinite(record.PhysicsBody.LastUpdateTime));
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 0.1, 1, 1));
Assert.Same(runtime, record.ProjectileRuntime);
}
[Fact]
public void FirstRemoteMissileStateWithInvalidLocalClock_AdoptsExistingBody()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new GameWindow.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, double.PositiveInfinity, 1, 1));
Assert.NotNull(record.ProjectileRuntime);
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
Assert.Equal(MissileState, remote.Body.State);
Assert.True(double.IsFinite(remote.Body.LastUpdateTime));
}
[Fact]
public void Bind_AppliesRetailFrictionValidationAndElasticityClamp()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(
instance: 1,
friction: -0.1f,
elasticity: 2f);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
Assert.Equal(PhysicsBody.DefaultFriction, record.PhysicsBody!.Friction);
Assert.Equal(0.1f, record.PhysicsBody.Elasticity);
}
[Fact]
public void Bind_AcceptsRetailZeroFrictionBoundary()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(
instance: 1,
friction: 0f,
elasticity: 0f);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
Assert.Equal(0f, record.PhysicsBody!.Friction);
Assert.Equal(0f, record.PhysicsBody.Elasticity);
}
[Fact]
public void RemoteMotionFirst_StateAcquisitionPreservesCurrentBodyAndCanonicalCell()
{
const uint startCell = 0xA9B40039u;
const uint destinationCell = 0xAAB40001u;
var fixture = new Fixture(loadInitialLandblock: false, BuildBoundaryEngine());
fixture.Spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu));
fixture.Spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu));
LiveEntityRecord record = fixture.Spawn(
instance: 1,
cellId: startCell,
worldPosition: new Vector3(191f, 10f, 50f),
cellLocalPosition: new Vector3(191f, 10f, 50f),
velocity: new Vector3(40f, 0f, 0f),
angularVelocity: new Vector3(0f, 0f, 2f));
WorldEntity entity = record.WorldEntity!;
fixture.Engine.ShadowObjects.Register(
entity.Id,
entity.SourceGfxObjOrSetupId,
entity.Position,
entity.Rotation,
radius: 0.5f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0xA9B4FFFFu,
seedCellId: startCell,
isStatic: false);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new GameWindow.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
Assert.Equal(new Vector3(40f, 0f, 0f), remote.Body.Velocity);
Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega);
remote.Body.set_velocity(new Vector3(8f, 0f, 0f));
remote.Body.Omega = new Vector3(0f, 0f, 3f);
remote.Body.Position = entity.Position;
remote.Body.State = record.FinalPhysicsState;
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
PhysicsBody body = remote.Body;
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid,
MissileState,
currentTime: 1.2,
liveCenterX: 0xA9,
liveCenterY: 0xB4));
Assert.Same(body, record.ProjectileRuntime!.Body);
Assert.Same(body, record.RemoteMotionRuntime!.Body);
Assert.Same(body, record.PhysicsBody);
Assert.Equal(new Vector3(8f, 0f, 0f), body.Velocity);
Assert.Equal(new Vector3(0f, 0f, 3f), body.Omega);
Assert.True(body.IsActive);
fixture.Controller.Tick(1.45, 0xA9, 0xB4, playerWorldPosition: null);
Assert.Equal(destinationCell, record.FullCellId);
Assert.Equal(destinationCell, remote.CellId);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 1.5, 0xA9, 0xB4));
Assert.Equal(destinationCell, remote.CellId);
Assert.Equal(destinationCell, record.FullCellId);
body.Position = new Vector3(191f, 10f, 50f);
entity.SetPosition(body.Position);
remote.CellId = startCell;
Assert.True(fixture.Controller.LeaveWorld(Guid));
fixture.Controller.Tick(1.6, 0xA9, 0xB4, playerWorldPosition: null);
Assert.True(body.InWorld);
Assert.Contains(
fixture.Engine.ShadowObjects.GetObjectsInCell(startCell),
entry => entry.EntityId == entity.Id);
Assert.DoesNotContain(
fixture.Engine.ShadowObjects.GetObjectsInCell(destinationCell),
entry => entry.EntityId == entity.Id);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 1.7, 0xA9, 0xB4));
Assert.Equal(startCell, record.FullCellId);
Assert.Equal(startCell, remote.CellId);
Assert.Equal(startCell, body.CellPosition.ObjCellId);
}
[Fact]
public void RemoteMotionFirst_ValidatesAdoptedBodyInsteadOfStaleCreateVectors()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(
instance: 1,
velocity: new Vector3(float.NaN, 0f, 0f));
WorldEntity entity = record.WorldEntity!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new GameWindow.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation;
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
Assert.True(float.IsNaN(record.Snapshot.Physics!.Value.Velocity!.Value.X));
Assert.Equal(Vector3.Zero, remote.Body.Velocity);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 1.0, 1, 1));
Assert.NotNull(record.ProjectileRuntime);
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
Assert.Equal(Vector3.Zero, remote.Body.Velocity);
Assert.Equal(entity.Position, remote.Body.Position);
}
[Fact]
public void RemoteMotionFirst_RejectsNonFiniteCurrentBodyDespiteValidCreateSnapshot()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new GameWindow.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation;
remote.Body.Velocity = new Vector3(float.PositiveInfinity, 0f, 0f);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 1.0, 1, 1));
Assert.Null(record.ProjectileRuntime);
Assert.Same(remote.Body, record.PhysicsBody);
Assert.True(float.IsPositiveInfinity(remote.Body.Velocity.X));
Assert.Equal(MissileState, remote.Body.State);
}
[Fact]
public void SharedRemoteMotionCell_DelegatesToCanonicalLiveRecord()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
var remote = new GameWindow.RemoteMotion(body);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
fixture.Live.RebucketLiveEntity(Guid, CellB);
Assert.Equal(CellB, remote.CellId);
remote.CellId = CellA;
Assert.Equal(CellA, record.FullCellId);
Assert.Equal(CellA, remote.CellId);
Assert.Same(body, record.RemoteMotionRuntime!.Body);
Assert.Same(body, record.ProjectileRuntime!.Body);
var replacement = new GameWindow.RemoteMotion(body);
fixture.Live.SetRemoteMotionRuntime(Guid, replacement);
remote.CellId = CellB;
Assert.Equal(CellA, record.FullCellId);
Assert.Equal(0u, remote.CellId);
replacement.CellId = CellB;
Assert.Equal(CellB, record.FullCellId);
}
[Fact]
public void PickupLeaveWorldAndPositionReentry_RestoresShadowWithoutClockBacklog()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 4);
WorldEntity entity = record.WorldEntity!;
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
fixture.Engine.ShadowObjects.Register(
entity.Id,
entity.SourceGfxObjOrSetupId,
entity.Position,
entity.Rotation,
radius: 0.5f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: CellA & 0xFFFF0000u,
seedCellId: CellA,
isStatic: false);
Assert.Contains(
fixture.Engine.ShadowObjects.GetObjectsInCell(CellA),
entry => entry.EntityId == entity.Id);
Assert.True(fixture.Live.TryApplyPickup(
new PickupEvent.Parsed(Guid, InstanceSequence: 4, PositionSequence: 2),
out _));
Assert.True(fixture.Live.WithdrawLiveEntityProjection(Guid));
Assert.True(fixture.Controller.LeaveWorld(Guid));
Assert.DoesNotContain(
fixture.Engine.ShadowObjects.GetObjectsInCell(CellA),
entry => entry.EntityId == entity.Id);
var correction = new CreateObject.ServerPosition(
CellA,
12f,
10f,
5f,
1f,
0f,
0f,
0f);
var update = new WorldSession.EntityPositionUpdate(
Guid,
correction,
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 4,
PositionSequence: 3,
TeleportSequence: 0,
ForcePositionSequence: 0);
Assert.True(fixture.Live.TryApplyPosition(
update,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out PositionTimestampDisposition disposition,
out WorldSession.EntitySpawn accepted,
out _));
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
Assert.Same(
entity,
fixture.Live.MaterializeLiveEntity(
Guid,
accepted.Position!.Value.LandblockId,
_ => throw new InvalidOperationException("re-entry recreated the entity")));
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
Guid,
worldPosition: new Vector3(12f, 10f, 5f),
cellLocalPosition: new Vector3(12f, 10f, 5f),
orientation: Quaternion.Identity,
fullCellId: CellA,
currentTime: 10.0,
liveCenterX: 1,
liveCenterY: 1));
Assert.Equal(10.0, record.PhysicsBody!.LastUpdateTime, 8);
Assert.Contains(
fixture.Engine.ShadowObjects.GetObjectsInCell(CellA),
entry => entry.EntityId == entity.Id);
fixture.Controller.Tick(
10.1,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: null);
Assert.Equal(13f, entity.Position.X, 3);
}
[Fact]
public void DeleteAndGuidReuse_IsolateProjectileGenerations()
{
var fixture = new Fixture();
LiveEntityRecord first = fixture.Spawn(instance: 7);
Assert.True(fixture.Controller.TryBind(first, ProjectileSetup(), 1.0));
WorldEntity firstEntity = first.WorldEntity!;
ILiveEntityProjectileRuntime firstRuntime = first.ProjectileRuntime!;
Assert.True(fixture.Live.UnregisterLiveEntity(
new DeleteObject.Parsed(Guid, InstanceSequence: 7),
isLocalPlayer: false));
LiveEntityRecord second = fixture.Spawn(instance: 8);
Assert.True(fixture.Controller.TryBind(second, ProjectileSetup(), 2.0));
Assert.NotSame(firstEntity, second.WorldEntity);
Assert.NotSame(firstRuntime, second.ProjectileRuntime);
Assert.Equal(1, fixture.Controller.Count);
Assert.Equal(2, fixture.Resources.Registers);
Assert.Equal(1, fixture.Resources.Unregisters);
}
[Fact]
public void MissingRetailSphere_FailsWithoutFabricatingCollisionBody()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
string? diagnostic = null;
fixture.Controller.DiagnosticSink = text => diagnostic = text;
Assert.False(fixture.Controller.TryBind(record, new Setup(), 1.0));
Assert.Null(record.ProjectileRuntime);
Assert.Null(record.PhysicsBody);
Assert.Contains("one-sphere", diagnostic);
}
private sealed class Fixture
{
internal Fixture(
bool loadInitialLandblock = true,
PhysicsEngine? physicsEngine = null)
{
if (loadInitialLandblock)
Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
Live = new LiveEntityRuntime(Spatial, Resources);
Engine = physicsEngine ?? new PhysicsEngine();
Controller = new ProjectileController(
Live,
Engine,
_ => ResolvedSetup,
entity => Poses.UpdateRoot(entity));
}
internal GpuWorldState Spatial { get; } = new();
internal RecordingResources Resources { get; } = new();
internal LiveEntityRuntime Live { get; }
internal PhysicsEngine Engine { get; }
internal EntityEffectPoseRegistry Poses { get; } = new();
internal Setup ResolvedSetup { get; set; } = ProjectileSetup();
internal ProjectileController Controller { get; }
internal LiveEntityRecord Spawn(
ushort instance,
ushort vectorSequence = 1,
uint cellId = CellA,
Vector3? worldPosition = null,
Vector3? cellLocalPosition = null,
Vector3? velocity = null,
Vector3? angularVelocity = null,
float friction = 0f,
float elasticity = 0f,
bool hasRenderablePart = false)
{
Vector3 world = worldPosition ?? new Vector3(10f, 10f, 5f);
Vector3 cellLocal = cellLocalPosition ?? world;
WorldSession.EntitySpawn spawn = ProjectileSpawn(
instance,
vectorSequence,
cellId,
cellLocal,
velocity ?? new Vector3(10f, 0f, 0f),
angularVelocity ?? Vector3.Zero,
friction,
elasticity);
LiveEntityRegistrationResult registration = Live.RegisterLiveEntity(spawn);
Assert.NotNull(registration.Record);
WorldEntity? entity = Live.MaterializeLiveEntity(
Guid,
cellId,
localId => new WorldEntity
{
Id = localId,
ServerGuid = Guid,
SourceGfxObjOrSetupId = 0x02000124u,
Position = world,
Rotation = Quaternion.Identity,
MeshRefs = hasRenderablePart
? new[] { new MeshRef(0x01000001u, Matrix4x4.Identity) }
: Array.Empty<MeshRef>(),
ParentCellId = cellId,
});
Assert.NotNull(entity);
Poses.PublishMeshRefs(entity);
return registration.Record!;
}
}
private static Setup ProjectileSetup(float radius = 0.102f) => new()
{
Spheres =
{
new Sphere
{
Origin = new Vector3(0f, -0.165f, 0.1045f),
Radius = radius,
},
},
};
private static WorldSession.EntitySpawn ProjectileSpawn(
ushort instance,
ushort vectorSequence,
uint cellId,
Vector3 cellLocalPosition,
Vector3 velocity,
Vector3 angularVelocity,
float friction,
float elasticity)
{
var position = new CreateObject.ServerPosition(
cellId,
cellLocalPosition.X,
cellLocalPosition.Y,
cellLocalPosition.Z,
1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: vectorSequence,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: instance);
var physics = new PhysicsSpawnData(
RawState: (uint)MissileState,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000124u,
MotionTableId: null,
SoundTableId: null,
PhysicsScriptTableId: 0x3400002Bu,
Parent: null,
Children: null,
Scale: 1f,
Friction: friction,
Elasticity: elasticity,
Translucency: null,
Velocity: velocity,
Acceleration: new Vector3(999f, 999f, 999f),
AngularVelocity: angularVelocity,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
Guid,
position,
0x02000124u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: 1f,
Name: "Projectile fixture",
ItemType: null,
MotionState: null,
MotionTableId: null,
PhysicsState: (uint)MissileState,
Friction: friction,
Elasticity: elasticity,
InstanceSequence: instance,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private static LoadedLandblock EmptyLandblock(uint canonicalId) =>
new(canonicalId, new LandBlock(), Array.Empty<WorldEntity>());
private static PhysicsEngine BuildBoundaryEngine()
{
static TerrainSurface EmptyTerrain()
{
var heights = new byte[81];
var table = new float[256];
Array.Fill(table, -1000f);
return new TerrainSurface(heights, table);
}
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.AddLandblock(
0xA9B4FFFFu,
EmptyTerrain(),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f);
engine.AddLandblock(
0xAAB4FFFFu,
EmptyTerrain(),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
192f,
0f);
return engine;
}
}