acdream/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs
Erik 1e98d81448 feat(vfx): port retail hidden and teleport presentation
Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
2026-07-14 14:59:48 +02:00

1318 lines
50 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 Hidden_PausesProjectileWithoutClearingActiveOrReplayingClockBacklog()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
fixture.Engine.ShadowObjects.Register(
entity.Id,
entity.SourceGfxObjOrSetupId,
entity.Position,
entity.Rotation,
radius: 0.5f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0x01010000u,
state: (uint)MissileState,
seedCellId: CellA);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
PhysicsBody body = record.PhysicsBody!;
Assert.True(body.IsActive);
record.FinalPhysicsState = MissileState
| PhysicsStateFlags.Hidden
| PhysicsStateFlags.IgnoreCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 1.1, 1, 1));
Vector3 hiddenPosition = entity.Position;
fixture.Controller.Tick(5.0, 1, 1, playerWorldPosition: null);
Assert.Equal(hiddenPosition, entity.Position);
Assert.True(body.InWorld);
Assert.True(body.IsActive);
Assert.Equal(5.0, body.LastUpdateTime, 8);
Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 5.0, 1, 1));
fixture.Controller.Tick(5.1, 1, 1, playerWorldPosition: null);
Assert.True(entity.Position.X > hiddenPosition.X);
Assert.True(entity.Position.X < hiddenPosition.X + 2f);
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
}
[Fact]
public void HiddenSharedRemoteProjectile_UsesPositionManagerNarrowTickExactlyOnce()
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
Assert.Null(record.AnimationRuntime);
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(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
record.FinalPhysicsState = MissileState
| PhysicsStateFlags.Hidden
| PhysicsStateFlags.IgnoreCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 1.1, 1, 1));
remote.Interp.Enqueue(
entity.Position + Vector3.UnitX,
heading: 0f,
isMovingTo: false,
currentBodyPosition: remote.Body.Position);
var updater = new RemotePhysicsUpdater(
fixture.Engine,
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
int publishedRoots = 0;
updater.TickHiddenEntities(
fixture.Live,
localPlayerServerGuid: 0x50000001u,
dt: 0.1f,
_ => publishedRoots++);
Vector3 afterNarrowTick = entity.Position;
fixture.Controller.Tick(1.2, 1, 1, playerWorldPosition: null);
Assert.True(afterNarrowTick.X > 10f);
Assert.Equal(afterNarrowTick, entity.Position);
Assert.Equal(1, publishedRoots);
Assert.True(fixture.Controller.HandlesMovement(Guid));
}
[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;
}
}