acdream/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs
Erik f961d70023 feat(physics): port retail complete object frame pipeline
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates.

Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-20 09:10:31 +02:00

1518 lines
59 KiB
C#

using System.Numerics;
using AcDream.App.Physics;
using AcDream.App.Rendering;
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;
namespace AcDream.App.Tests.Physics;
public sealed class RemoteTeleportControllerTests
{
private sealed class Resources : ILiveEntityResourceLifecycle
{
public void Register(WorldEntity entity) { }
public void Unregister(WorldEntity entity) { }
}
private sealed class BodyOnlyRemote(PhysicsBody body) : ILiveEntityRemoteMotionRuntime
{
public PhysicsBody Body { get; } = body;
}
private sealed class MutablePlacementRemote(PhysicsBody body) : ILiveEntityRemotePlacementRuntime
{
private uint _cellId;
private Func<uint>? _readCell;
private Action<uint>? _writeCell;
public PhysicsBody Body { get; set; } = body;
public uint CellId
{
get => _readCell?.Invoke() ?? _cellId;
set
{
_cellId = value;
_writeCell?.Invoke(value);
}
}
public bool Airborne { get; set; }
public Vector3 LastServerPosition { get; set; }
public double LastServerPositionTime { get; set; }
public Vector3 LastShadowSyncPosition { get; set; }
public Quaternion LastShadowSyncOrientation { get; set; }
public void BindCanonicalCell(Func<uint> read, Action<uint> write)
{
_readCell = read;
_writeCell = write;
}
public void HitGround() { }
public void LeaveGround() { }
}
private sealed class AlternatingBodyRemote(
PhysicsBody first,
PhysicsBody later) : ILiveEntityRemotePlacementRuntime
{
private int _reads;
public PhysicsBody Body => _reads++ == 0 ? first : later;
public uint CellId { get; set; }
public bool Airborne { get; set; }
public Vector3 LastServerPosition { get; set; }
public double LastServerPositionTime { get; set; }
public Vector3 LastShadowSyncPosition { get; set; }
public Quaternion LastShadowSyncOrientation { get; set; }
public void BindCanonicalCell(Func<uint> read, Action<uint> write) { }
public void HitGround() { }
public void LeaveGround() { }
}
private sealed class AlternatingProjectileRuntime(
PhysicsBody first,
PhysicsBody later) : ILiveEntityProjectileRuntime
{
private int _reads;
public PhysicsBody Body => _reads++ == 0 ? first : later;
}
[Fact]
public void PendingDestination_ParksAuthoritativeFrameThenResolvesContactOnLoad()
{
const uint guid = 0x70000001u;
const uint sourceCell = 0xA9B40039u;
const uint destinationCell = 0xAAB40001u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu));
var live = new LiveEntityRuntime(spatial, new Resources());
live.RegisterLiveEntity(Spawn(guid, sourceCell));
WorldEntity entity = live.MaterializeLiveEntity(
guid,
sourceCell,
id => new WorldEntity
{
Id = id,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(191f, 10f, 50f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var remote = new GameWindow.RemoteMotion();
remote.Body.SnapToCell(
sourceCell,
entity.Position,
entity.Position);
remote.Body.TransientState = TransientStateFlags.Contact
| TransientStateFlags.OnWalkable;
live.SetRemoteMotionRuntime(guid, remote);
var engine = BuildEngine(includeDestination: false);
engine.ShadowObjects.Register(
entity.Id,
entity.SourceGfxObjOrSetupId,
entity.Position,
entity.Rotation,
radius: 0.48f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0xA9B4FFFFu,
collisionType: ShadowCollisionType.Cylinder,
cylHeight: 1.835f,
seedCellId: sourceCell,
isStatic: false);
Assert.True(engine.ShadowObjects.Suspend(entity.Id));
int shadowSyncs = 0;
using var controller = new RemoteTeleportController(
engine,
live,
(_, _) => (0.48f, 1.835f),
CellLocal,
(resolvedEntity, resolvedRemote, resolvedCell) =>
{
shadowSyncs++;
engine.ShadowObjects.Register(
resolvedEntity.Id,
resolvedEntity.SourceGfxObjOrSetupId,
resolvedRemote.Position,
resolvedRemote.Orientation,
radius: 0.48f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0xAAB4FFFFu,
collisionType: ShadowCollisionType.Cylinder,
cylHeight: 1.835f,
seedCellId: resolvedCell,
isStatic: false);
},
(_, _, _) => { },
(_, _) => { });
Assert.True(live.RebucketLiveEntity(guid, destinationCell));
var result = controller.TryApply(
remote,
entity,
new Vector3(193f, 10f, 50f),
destinationCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 5.0,
destinationProjectionVisible: false,
generation: 1,
positionSequence: 1);
Assert.True(result.Applied);
Assert.False(result.ContactResolved);
Assert.Equal(destinationCell, remote.CellId);
Assert.Equal(new Vector3(193f, 10f, 50f), entity.Position);
Assert.False(remote.Body.InContact);
Assert.Equal(0, engine.ShadowObjects.TotalRegistered);
Assert.True(live.TryGetRecord(guid, out LiveEntityRecord pending));
Assert.False(pending.IsSpatiallyVisible);
Assert.True(live.TryApplyPosition(
new WorldSession.EntityPositionUpdate(
guid,
new CreateObject.ServerPosition(
destinationCell, 2f, 10f, 50f, 1f, 0f, 0f, 0f),
null,
null,
true,
1,
2,
0,
0),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
var latest = controller.TryApply(
remote,
entity,
new Vector3(194f, 10f, 50f),
destinationCell,
new Vector3(2f, 10f, 50f),
Quaternion.Identity,
gameTime: 6.0,
destinationProjectionVisible: false,
generation: 1,
positionSequence: 2);
Assert.True(latest.Applied);
Assert.False(latest.ContactResolved);
AddDestination(engine);
spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu));
Assert.True(pending.IsSpatiallyVisible);
Assert.Equal(destinationCell, pending.FullCellId);
Assert.True(remote.Body.InContact);
Assert.True(remote.Body.OnWalkable);
Assert.False(remote.Airborne);
Assert.Equal(194f, entity.Position.X, 3);
Assert.Equal(remote.Body.Position, entity.Position);
Assert.Equal(1, shadowSyncs);
Assert.Equal(1, engine.ShadowObjects.TotalRegistered);
ShadowEntry restored = Assert.Single(
engine.ShadowObjects.AllEntriesForDebug());
Assert.Equal(entity.Position, restored.Position);
}
[Fact]
public void LoadedPlacementFailure_RestoresSuspendedShadowAtRetainedPose()
{
const uint guid = 0x70000002u;
const uint sourceCell = 0xA9B40039u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu));
var live = new LiveEntityRuntime(spatial, new Resources());
live.RegisterLiveEntity(Spawn(guid, sourceCell));
WorldEntity entity = live.MaterializeLiveEntity(
guid,
sourceCell,
id => new WorldEntity
{
Id = id,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(10f, 10f, 50f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var remote = new GameWindow.RemoteMotion();
remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position);
live.SetRemoteMotionRuntime(guid, remote);
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.ShadowObjects.Register(
entity.Id,
entity.SourceGfxObjOrSetupId,
entity.Position,
entity.Rotation,
0.48f,
0f,
0f,
0xA9B4FFFFu,
ShadowCollisionType.Cylinder,
1.835f,
seedCellId: sourceCell,
isStatic: false);
Assert.True(engine.ShadowObjects.Suspend(entity.Id));
int shadowSyncs = 0;
using var controller = new RemoteTeleportController(
engine,
live,
(_, _) => (0.48f, 1.835f),
CellLocal,
(resolvedEntity, resolvedRemote, resolvedCell) =>
{
shadowSyncs++;
engine.ShadowObjects.Register(
resolvedEntity.Id,
resolvedEntity.SourceGfxObjOrSetupId,
resolvedRemote.Position,
resolvedRemote.Orientation,
0.48f,
0f,
0f,
0xA9B4FFFFu,
ShadowCollisionType.Cylinder,
1.835f,
seedCellId: resolvedCell,
isStatic: false);
},
(_, _, _) => { },
(_, _) => { },
resolvePlacement: (position, cell, _, _, _, _) =>
new ResolveResult(
position,
cell,
IsOnGround: false,
Ok: false));
RemoteTeleportController.Result result = controller.TryApply(
remote,
entity,
new Vector3(20f, 20f, 50f),
requestedCellId: 0u,
requestedCellLocalPosition: Vector3.Zero,
requestedOrientation: Quaternion.Identity,
gameTime: 2.0,
destinationProjectionVisible: true,
generation: 1,
positionSequence: 1);
Assert.False(result.Applied);
Assert.Equal(1, shadowSyncs);
Assert.Equal(1, engine.ShadowObjects.TotalRegistered);
Assert.Equal(new Vector3(10f, 10f, 50f), entity.Position);
}
[Fact]
public void DeferredPlacementFailure_RollsBackProjectionBodyAndShadow()
{
const uint guid = 0x70000003u;
const uint sourceCell = 0xA9B40039u;
const uint destinationCell = 0xAAB40001u;
Vector3 sourcePosition = new(191f, 10f, 50f);
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu));
var live = new LiveEntityRuntime(spatial, new Resources());
live.RegisterLiveEntity(Spawn(guid, sourceCell));
WorldEntity entity = live.MaterializeLiveEntity(
guid,
sourceCell,
id => new WorldEntity
{
Id = id,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = sourcePosition,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var remote = new GameWindow.RemoteMotion();
remote.Body.SnapToCell(sourceCell, sourcePosition, sourcePosition);
remote.Body.TransientState = TransientStateFlags.Contact
| TransientStateFlags.OnWalkable;
remote.CellId = sourceCell;
live.SetRemoteMotionRuntime(guid, remote);
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.ShadowObjects.Register(
entity.Id,
entity.SourceGfxObjOrSetupId,
sourcePosition,
Quaternion.Identity,
0.48f,
0f,
0f,
0xA9B4FFFFu,
ShadowCollisionType.Cylinder,
1.835f,
seedCellId: sourceCell,
isStatic: false);
Assert.True(engine.ShadowObjects.Suspend(entity.Id));
int shadowSyncs = 0;
using var controller = new RemoteTeleportController(
engine,
live,
(_, _) => (0.48f, 1.835f),
CellLocal,
(resolvedEntity, resolvedRemote, resolvedCell) =>
{
shadowSyncs++;
engine.ShadowObjects.Register(
resolvedEntity.Id,
resolvedEntity.SourceGfxObjOrSetupId,
resolvedRemote.Position,
resolvedRemote.Orientation,
0.48f,
0f,
0f,
0xA9B4FFFFu,
ShadowCollisionType.Cylinder,
1.835f,
seedCellId: resolvedCell,
isStatic: false);
},
(_, _, _) => { },
(_, _) => { },
resolvePlacement: (position, cell, _, _, _, _) =>
new ResolveResult(
position,
cell,
IsOnGround: false,
Ok: false));
Assert.True(live.RebucketLiveEntity(guid, destinationCell));
RemoteTeleportController.Result parked = controller.TryApply(
remote,
entity,
new Vector3(193f, 10f, 50f),
destinationCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 5.0,
destinationProjectionVisible: false,
generation: 1,
positionSequence: 1);
Assert.True(parked.Applied);
Assert.True(controller.HasPending(guid));
// Rendering becomes resident without matching physics collision data.
// The visibility edge is the one resolution attempt; failure must
// restore the retained source instead of leaving a visible ghost.
spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu));
Assert.False(controller.HasPending(guid));
Assert.Equal(sourcePosition, remote.Body.Position);
Assert.Equal(sourceCell, remote.CellId);
Assert.Equal(sourcePosition, entity.Position);
Assert.Equal(sourceCell, entity.ParentCellId);
Assert.True(remote.Body.InContact);
Assert.True(remote.Body.OnWalkable);
Assert.True(live.TryGetRecord(guid, out LiveEntityRecord restored));
Assert.Equal(sourceCell, restored.FullCellId);
Assert.True(restored.IsSpatiallyVisible);
Assert.Equal(1, shadowSyncs);
Assert.Equal(1, engine.ShadowObjects.TotalRegistered);
}
[Fact]
public void DeferredFailure_WithUnloadedSource_RestoresShadowWhenSourceReloads()
{
using var fixture = new RollbackFixture();
Assert.True(fixture.ParkPending().Applied);
fixture.VisibilityEdges.Clear();
fixture.Spatial.RemoveLandblock(RollbackFixture.SourceLandblock);
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock));
Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid));
Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord pendingSource));
Assert.Equal(RollbackFixture.SourceCell, pendingSource.FullCellId);
Assert.False(pendingSource.IsSpatiallyVisible);
Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered);
Assert.False(fixture.PresentationVisible);
Assert.Equal(new[] { true, false }, fixture.VisibilityEdges);
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.SourceLandblock));
Assert.True(pendingSource.IsSpatiallyVisible);
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(1, fixture.ShadowSyncs);
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
Assert.True(fixture.PresentationVisible);
Assert.Equal(new[] { true, false, true }, fixture.VisibilityEdges);
}
[Fact]
public void DeferredFailure_WithCelllessSource_WithdrawsPresentationWithoutShadowRestore()
{
using var fixture = new RollbackFixture(celllessSource: true);
Assert.True(fixture.ParkPending().Applied);
Assert.False(fixture.PresentationVisible);
fixture.VisibilityEdges.Clear();
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock));
Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid));
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord withdrawn));
Assert.Equal(0u, withdrawn.FullCellId);
Assert.False(withdrawn.IsSpatiallyProjected);
Assert.False(withdrawn.IsSpatiallyVisible);
Assert.False(fixture.PresentationVisible);
Assert.Equal(0, fixture.ShadowSyncs);
Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered);
Assert.Equal(new[] { true, false }, fixture.VisibilityEdges);
}
[Fact]
public void DeferredFailure_WithLoadedSource_PublishesNoFalseRebucketPulse()
{
using var fixture = new RollbackFixture();
Assert.True(fixture.ParkPending().Applied);
fixture.VisibilityEdges.Clear();
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock));
Assert.Equal(new[] { true }, fixture.VisibilityEdges);
Assert.True(fixture.PresentationVisible);
Assert.Equal(RollbackFixture.SourcePosition, fixture.Entity.Position);
Assert.Equal(RollbackFixture.SourceCell, fixture.Entity.ParentCellId);
Assert.Equal(1, fixture.ShadowSyncs);
}
[Fact]
public void NewerLoadedFailure_ReplacesPendingAndRestoresOriginalSource()
{
const uint newerCell = 0xABB40001u;
const uint newerLandblock = 0xABB4FFFFu;
using var fixture = new RollbackFixture();
fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock));
Assert.True(fixture.ParkPending().Applied);
Assert.True(fixture.Live.TryApplyPosition(
new WorldSession.EntityPositionUpdate(
RollbackFixture.Guid,
new CreateObject.ServerPosition(
newerCell, 1f, 10f, 50f, 1f, 0f, 0f, 0f),
null,
null,
true,
1,
2,
0,
0),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
fixture.VisibilityEdges.Clear();
fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1);
Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell));
Assert.True(fixture.Controller.HasPending(RollbackFixture.Guid));
Assert.Equal(new[] { true }, fixture.VisibilityEdges);
RemoteTeleportController.Result failed = fixture.Controller.TryApply(
fixture.Remote,
fixture.Entity,
new Vector3(385f, 10f, 50f),
newerCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 6.0,
destinationProjectionVisible: true,
generation: 1,
positionSequence: 2);
Assert.False(failed.Applied);
Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid));
Assert.Equal(RollbackFixture.SourcePosition, fixture.Remote.Body.Position);
Assert.Equal(RollbackFixture.SourceCell, fixture.Remote.CellId);
Assert.True(fixture.Remote.Body.InContact);
Assert.True(fixture.Remote.Body.OnWalkable);
Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord restored));
Assert.Equal(RollbackFixture.SourceCell, restored.FullCellId);
Assert.True(restored.IsSpatiallyVisible);
Assert.Equal(1, fixture.ShadowSyncs);
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
Assert.Equal(new[] { true }, fixture.VisibilityEdges);
}
[Fact]
public void HiddenThenUnhideWhileRollbackSourcePending_RestoresShadowExactlyOnce()
{
using var fixture = new RollbackFixture();
Assert.True(fixture.ParkPending().Applied);
fixture.Spatial.RemoveLandblock(RollbackFixture.SourceLandblock);
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock));
Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.True(fixture.Live.TryApplyState(
new SetState.Parsed(
RollbackFixture.Guid,
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
1,
2),
out _,
out _));
Assert.True(fixture.Presentation.OnStateAccepted(RollbackFixture.Guid));
Assert.True(fixture.Live.TryApplyState(
new SetState.Parsed(
RollbackFixture.Guid,
(uint)PhysicsStateFlags.ReportCollisions,
1,
3),
out _,
out _));
Assert.True(fixture.Presentation.OnStateAccepted(RollbackFixture.Guid));
Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.SourceLandblock));
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(1, fixture.ShadowSyncs);
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void DeferredRollback_NewerLoadedPlacementTransfersShadowUntilResolution(
bool resolutionSucceeds)
{
const uint newerCell = 0xABB40001u;
const uint newerLandblock = 0xABB4FFFFu;
using var fixture = new RollbackFixture();
CreateUnloadedSourceRollback(fixture);
fixture.ResolveSucceeds = resolutionSucceeds;
fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock));
AcceptNewerPosition(fixture, newerCell);
fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1);
fixture.VisibilityEdges.Clear();
Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell));
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(0, fixture.ShadowSyncs);
RemoteTeleportController.Result result = fixture.Controller.TryApply(
fixture.Remote,
fixture.Entity,
new Vector3(385f, 10f, 50f),
newerCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 7.0,
destinationProjectionVisible: true,
generation: 1,
positionSequence: 2);
Assert.Equal(resolutionSucceeds, result.Applied);
Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.ShadowSyncs);
Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.Engine.ShadowObjects.TotalRegistered);
Assert.Equal(!resolutionSucceeds,
fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord record));
Assert.Equal(
resolutionSucceeds ? newerCell : RollbackFixture.SourceCell,
record.FullCellId);
Assert.Equal(resolutionSucceeds, record.IsSpatiallyVisible);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void DeferredRollback_NewerPendingHydrationTransfersShadowUntilResolution(
bool resolutionSucceeds)
{
const uint newerCell = 0xACB40001u;
const uint newerLandblock = 0xACB4FFFFu;
using var fixture = new RollbackFixture();
CreateUnloadedSourceRollback(fixture);
fixture.ResolveSucceeds = resolutionSucceeds;
AcceptNewerPosition(fixture, newerCell);
fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1);
Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell));
Assert.True(fixture.Controller.TryApply(
fixture.Remote,
fixture.Entity,
new Vector3(577f, 10f, 50f),
newerCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 7.0,
destinationProjectionVisible: false,
generation: 1,
positionSequence: 2).Applied);
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(0, fixture.ShadowSyncs);
fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock));
Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid));
Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.ShadowSyncs);
Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.Engine.ShadowObjects.TotalRegistered);
Assert.Equal(!resolutionSucceeds,
fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord record));
Assert.Equal(
resolutionSucceeds ? newerCell : RollbackFixture.SourceCell,
record.FullCellId);
Assert.Equal(resolutionSucceeds, record.IsSpatiallyVisible);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void HiddenPlacement_UnhideBeforeHydrationWaitsForResolution(
bool resolutionSucceeds)
{
const uint newerCell = 0xACB40001u;
const uint newerLandblock = 0xACB4FFFFu;
using var fixture = new RollbackFixture();
CreateUnloadedSourceRollback(fixture);
fixture.ResolveSucceeds = resolutionSucceeds;
ApplyHidden(fixture, stateSequence: 2);
AcceptNewerPosition(fixture, newerCell);
fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1);
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell));
Assert.True(fixture.Controller.TryApply(
fixture.Remote,
fixture.Entity,
new Vector3(577f, 10f, 50f),
newerCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 7.0,
destinationProjectionVisible: false,
generation: 1,
positionSequence: 2).Applied);
ApplyVisible(fixture, stateSequence: 3);
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(0, fixture.ShadowSyncs);
fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock));
Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.ShadowSyncs);
Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.Engine.ShadowObjects.TotalRegistered);
Assert.Equal(!resolutionSucceeds,
fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void PendingPlacement_HideUnhideOscillationCannotRestoreBeforeResolution(
bool resolutionSucceeds)
{
const uint newerCell = 0xADB40001u;
const uint newerLandblock = 0xADB4FFFFu;
using var fixture = new RollbackFixture();
CreateUnloadedSourceRollback(fixture);
fixture.ResolveSucceeds = resolutionSucceeds;
AcceptNewerPosition(fixture, newerCell);
fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1);
Assert.True(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid));
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell));
Assert.True(fixture.Controller.TryApply(
fixture.Remote,
fixture.Entity,
new Vector3(769f, 10f, 50f),
newerCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 7.0,
destinationProjectionVisible: false,
generation: 1,
positionSequence: 2).Applied);
ApplyHidden(fixture, stateSequence: 2);
ApplyVisible(fixture, stateSequence: 3);
ApplyHidden(fixture, stateSequence: 4);
ApplyVisible(fixture, stateSequence: 5);
Assert.True(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid));
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(0, fixture.ShadowSyncs);
Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered);
fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock));
Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid));
Assert.False(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid));
Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.ShadowSyncs);
Assert.Equal(resolutionSucceeds ? 1 : 0, fixture.Engine.ShadowObjects.TotalRegistered);
Assert.Equal(!resolutionSucceeds,
fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void PendingPlacement_SameBodyRuntimeRebindFinishesStableOwnership(
bool resolutionSucceeds)
{
using var fixture = new RollbackFixture();
fixture.ResolveSucceeds = resolutionSucceeds;
Assert.True(fixture.ParkPending().Applied);
Assert.True(fixture.Controller.HasPending(RollbackFixture.Guid));
Assert.True(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid));
Assert.Throws<InvalidOperationException>(() =>
fixture.Live.SetRemoteMotionRuntime(
RollbackFixture.Guid,
new GameWindow.RemoteMotion()));
Assert.Throws<InvalidOperationException>(() =>
fixture.Live.SetRemoteMotionRuntime(
RollbackFixture.Guid,
new BodyOnlyRemote(fixture.Remote.Body)));
var replacement = new GameWindow.RemoteMotion(fixture.Remote.Body);
fixture.Live.SetRemoteMotionRuntime(RollbackFixture.Guid, replacement);
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock));
Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid));
Assert.False(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid));
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(1, fixture.ShadowSyncs);
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord stable));
Assert.Same(replacement, stable.RemoteMotionRuntime);
Assert.Equal(
resolutionSucceeds
? RollbackFixture.DestinationCell
: RollbackFixture.SourceCell,
stable.FullCellId);
Assert.Equal(stable.FullCellId, replacement.CellId);
}
[Fact]
public void PendingPlacement_RuntimeClearRollsBackAndCompletesOwnership()
{
using var fixture = new RollbackFixture();
Assert.True(fixture.ParkPending().Applied);
Assert.True(fixture.Live.ClearRemoteMotionRuntime(RollbackFixture.Guid));
Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord retained));
Assert.Same(fixture.Remote.Body, retained.PhysicsBody);
Assert.Throws<InvalidOperationException>(() =>
fixture.Live.SetRemoteMotionRuntime(
RollbackFixture.Guid,
new GameWindow.RemoteMotion()));
Assert.Throws<InvalidOperationException>(() =>
fixture.Live.SetRemoteMotionRuntime(
RollbackFixture.Guid,
new BodyOnlyRemote(fixture.Remote.Body)));
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock));
Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid));
Assert.False(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid));
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(1, fixture.ShadowSyncs);
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord stable));
Assert.Equal(RollbackFixture.SourceCell, stable.FullCellId);
Assert.Null(stable.RemoteMotionRuntime);
}
[Fact]
public void PendingPlacement_MutatedWrapperBodyIsDetachedAndCanonicalBodyRollsBack()
{
using var fixture = new RollbackFixture();
Assert.True(fixture.ParkPending().Applied);
PhysicsBody canonicalBody = fixture.Remote.Body;
var mutable = new MutablePlacementRemote(canonicalBody);
fixture.Live.SetRemoteMotionRuntime(RollbackFixture.Guid, mutable);
mutable.Body = new PhysicsBody();
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock));
Assert.False(fixture.Controller.HasPending(RollbackFixture.Guid));
Assert.False(fixture.Presentation.HasActivePlacement(RollbackFixture.Guid));
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(1, fixture.ShadowSyncs);
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord stable));
Assert.Same(canonicalBody, stable.PhysicsBody);
Assert.Null(stable.RemoteMotionRuntime);
Assert.Equal(RollbackFixture.SourcePosition, canonicalBody.Position);
Assert.Equal(RollbackFixture.SourcePosition, fixture.Entity.Position);
Assert.Equal(RollbackFixture.SourceCell, stable.FullCellId);
}
[Fact]
public void RuntimeBinding_SnapshotsDynamicBodyGetterOncePerBoundary()
{
using var fixture = new RollbackFixture();
PhysicsBody canonicalBody = fixture.Remote.Body;
var otherBody = new PhysicsBody();
var remote = new AlternatingBodyRemote(canonicalBody, otherBody);
fixture.Live.SetRemoteMotionRuntime(RollbackFixture.Guid, remote);
Assert.True(fixture.Live.TryGetRecord(RollbackFixture.Guid, out LiveEntityRecord record));
Assert.Same(canonicalBody, record.PhysicsBody);
Assert.Same(remote, record.RemoteMotionRuntime);
var projectile = new AlternatingProjectileRuntime(canonicalBody, otherBody);
fixture.Live.SetProjectileRuntime(RollbackFixture.Guid, projectile);
Assert.Same(canonicalBody, record.PhysicsBody);
Assert.Same(projectile, record.ProjectileRuntime);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void HiddenPlacement_HydrationHandsRestoreBackToUnhide(
bool resolutionSucceeds)
{
const uint newerCell = 0xACB40001u;
const uint newerLandblock = 0xACB4FFFFu;
using var fixture = new RollbackFixture();
CreateUnloadedSourceRollback(fixture);
fixture.ResolveSucceeds = resolutionSucceeds;
ApplyHidden(fixture, stateSequence: 2);
AcceptNewerPosition(fixture, newerCell);
fixture.Controller.BeginPlacement(RollbackFixture.Guid, generation: 1);
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.True(fixture.Live.RebucketLiveEntity(RollbackFixture.Guid, newerCell));
Assert.True(fixture.Controller.TryApply(
fixture.Remote,
fixture.Entity,
new Vector3(577f, 10f, 50f),
newerCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 7.0,
destinationProjectionVisible: false,
generation: 1,
positionSequence: 2).Applied);
fixture.Spatial.AddLandblock(EmptyLandblock(newerLandblock));
Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(0, fixture.ShadowSyncs);
Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered);
ApplyVisible(fixture, stateSequence: 3);
if (!resolutionSucceeds)
{
Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(0, fixture.ShadowSyncs);
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.SourceLandblock));
}
Assert.False(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(1, fixture.ShadowSyncs);
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
}
[Fact]
public void ResolverAcceptsNewerPosition_OlderPlacementReturnsSuperseded()
{
const uint guid = 0x70000060u;
const uint sourceCell = 0xA9B40039u;
const uint destinationCell = 0xAAB40001u;
var fixture = CreateLoadedRemoteFixture(guid, sourceCell);
Assert.True(fixture.Live.TryApplyPosition(
PositionUpdate(guid, destinationCell, x: 1f, sequence: 2),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
Assert.True(fixture.Live.RebucketLiveEntity(guid, destinationCell));
Assert.True(fixture.Live.TryGetRecord(guid, out LiveEntityRecord record));
ulong outerPositionAuthority = record.PositionAuthorityVersion;
ulong outerVelocityAuthority = record.VelocityAuthorityVersion;
Vector3 newerWorldPosition = new(195f, 10f, 50f);
RemoteTeleportController? controller = null;
RemoteTeleportController.Result nestedResult = default;
bool nested = false;
controller = new RemoteTeleportController(
fixture.Engine,
fixture.Live,
(_, _) => (0.48f, 1.835f),
CellLocal,
(_, _, _) => { },
(_, _, _) => { },
(_, _) => { },
resolvePlacement: (position, cell, _, _, _, _) =>
{
if (!nested)
{
nested = true;
Assert.True(fixture.Live.TryApplyPosition(
PositionUpdate(guid, destinationCell, x: 3f, sequence: 3),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
Assert.True(fixture.Live.TryGetRecord(guid, out var current));
nestedResult = controller!.TryApply(
current,
current.PositionAuthorityVersion,
current.VelocityAuthorityVersion,
fixture.Remote,
fixture.Entity,
newerWorldPosition,
destinationCell,
new Vector3(3f, 10f, 50f),
Quaternion.Identity,
gameTime: 6.0,
destinationProjectionVisible: true,
generation: 1,
positionSequence: 3);
}
return SuccessfulPlacement(position, cell);
});
RemoteTeleportController.Result outerResult = controller.TryApply(
record,
outerPositionAuthority,
outerVelocityAuthority,
fixture.Remote,
fixture.Entity,
new Vector3(193f, 10f, 50f),
destinationCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 5.0,
destinationProjectionVisible: true,
generation: 1,
positionSequence: 2);
Assert.True(nestedResult.Applied);
Assert.True(nestedResult.ContactResolved);
Assert.True(outerResult.Superseded);
Assert.False(outerResult.Applied);
Assert.Equal(newerWorldPosition, fixture.Remote.Body.Position);
Assert.Equal(newerWorldPosition, fixture.Entity.Position);
controller.Dispose();
}
[Fact]
public void ResolverAcceptsIndependentState_PositionPlacementStillCommits()
{
const uint guid = 0x70000061u;
const uint sourceCell = 0xA9B40039u;
const uint destinationCell = 0xAAB40001u;
var fixture = CreateLoadedRemoteFixture(guid, sourceCell);
Assert.True(fixture.Live.TryApplyPosition(
PositionUpdate(guid, destinationCell, x: 1f, sequence: 2),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
Assert.True(fixture.Live.RebucketLiveEntity(guid, destinationCell));
Assert.True(fixture.Live.TryGetRecord(guid, out LiveEntityRecord record));
ulong positionAuthority = record.PositionAuthorityVersion;
ulong velocityAuthority = record.VelocityAuthorityVersion;
bool stateAccepted = false;
using var controller = new RemoteTeleportController(
fixture.Engine,
fixture.Live,
(_, _) => (0.48f, 1.835f),
CellLocal,
(_, _, _) => { },
(_, _, _) => { },
(_, _) => { },
resolvePlacement: (position, cell, _, _, _, _) =>
{
if (!stateAccepted)
{
stateAccepted = true;
Assert.True(fixture.Live.TryApplyState(
new SetState.Parsed(
guid,
(uint)(PhysicsStateFlags.Hidden
| PhysicsStateFlags.ReportCollisions),
1,
2),
out _));
}
return SuccessfulPlacement(position, cell);
});
Vector3 destination = new(193f, 10f, 50f);
RemoteTeleportController.Result result = controller.TryApply(
record,
positionAuthority,
velocityAuthority,
fixture.Remote,
fixture.Entity,
destination,
destinationCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 5.0,
destinationProjectionVisible: true,
generation: 1,
positionSequence: 2);
Assert.True(stateAccepted);
Assert.True(result.Applied);
Assert.True(result.ContactResolved);
Assert.False(result.Superseded);
Assert.Equal(destination, fixture.Remote.Body.Position);
Assert.Equal(destination, fixture.Entity.Position);
Assert.True(record.FinalPhysicsState.HasFlag(PhysicsStateFlags.Hidden));
Assert.Equal(record.FinalPhysicsState, fixture.Remote.Body.State);
}
[Fact]
public void TeardownCallbackFailure_StillForgetsPendingBeforeGuidReuse()
{
const uint guid = 0x70000004u;
const uint sourceCell = 0xA9B40039u;
const uint destinationCell = 0xAAB40001u;
RemoteTeleportController? controller = null;
bool cleanupAfterFailureRan = false;
var resources = new DelegateLiveEntityResourceLifecycle(
_ => { },
_ => LiveEntityTeardown.Run(
[
() => throw new InvalidOperationException("effect teardown failed"),
() => controller!.Forget(guid),
() => cleanupAfterFailureRan = true,
]));
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu));
var live = new LiveEntityRuntime(spatial, resources);
live.RegisterLiveEntity(Spawn(guid, sourceCell));
WorldEntity entity = live.MaterializeLiveEntity(
guid,
sourceCell,
id => new WorldEntity
{
Id = id,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(191f, 10f, 50f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var remote = new GameWindow.RemoteMotion();
remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position);
live.SetRemoteMotionRuntime(guid, remote);
controller = new RemoteTeleportController(
BuildEngine(includeDestination: false),
live,
(_, _) => (0.48f, 1.835f),
CellLocal,
(_, _, _) => { },
(_, _, _) => { },
(_, _) => { });
Assert.True(live.RebucketLiveEntity(guid, destinationCell));
Assert.True(controller.TryApply(
remote,
entity,
new Vector3(193f, 10f, 50f),
destinationCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 5.0,
destinationProjectionVisible: false,
generation: 1,
positionSequence: 1).Applied);
Assert.True(controller.HasPending(guid));
Assert.Throws<AggregateException>(() => live.UnregisterLiveEntity(
new DeleteObject.Parsed(guid, InstanceSequence: 1),
isLocalPlayer: false));
Assert.True(cleanupAfterFailureRan);
Assert.False(controller.HasPending(guid));
Assert.False(live.TryGetRecord(guid, out _));
LiveEntityRegistrationResult replacement =
live.RegisterLiveEntity(Spawn(guid, sourceCell));
Assert.True(replacement.LogicalRegistrationCreated);
Assert.False(controller.HasPending(guid));
controller.Dispose();
}
private static PhysicsEngine BuildEngine(bool includeDestination)
{
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.AddLandblock(
0xA9B4FFFFu,
FlatTerrain(),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f);
if (includeDestination)
AddDestination(engine);
return engine;
}
private static LoadedRemoteFixture CreateLoadedRemoteFixture(
uint guid,
uint sourceCell)
{
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu));
spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu));
var live = new LiveEntityRuntime(spatial, new Resources());
live.RegisterLiveEntity(Spawn(guid, sourceCell));
WorldEntity entity = live.MaterializeLiveEntity(
guid,
sourceCell,
id => new WorldEntity
{
Id = id,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(191f, 10f, 50f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var remote = new GameWindow.RemoteMotion();
remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position);
live.SetRemoteMotionRuntime(guid, remote);
return new LoadedRemoteFixture(
live,
entity,
remote,
BuildEngine(includeDestination: true));
}
private static WorldSession.EntityPositionUpdate PositionUpdate(
uint guid,
uint cell,
float x,
ushort sequence) => new(
guid,
new CreateObject.ServerPosition(
cell,
x,
10f,
50f,
1f,
0f,
0f,
0f),
null,
null,
true,
1,
sequence,
0,
0);
private static ResolveResult SuccessfulPlacement(Vector3 position, uint cell) => new(
position,
cell,
IsOnGround: true,
InContact: true,
OnWalkable: true,
ContactPlane: new Plane(Vector3.UnitZ, 0f),
ContactPlaneCellId: cell);
private readonly record struct LoadedRemoteFixture(
LiveEntityRuntime Live,
WorldEntity Entity,
GameWindow.RemoteMotion Remote,
PhysicsEngine Engine);
private static void AddDestination(PhysicsEngine engine) =>
engine.AddLandblock(
0xAAB4FFFFu,
FlatTerrain(),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
192f,
0f);
private static TerrainSurface FlatTerrain()
{
var heights = new byte[81];
var table = new float[256];
Array.Fill(table, 50f);
return new TerrainSurface(heights, table);
}
private static Vector3 CellLocal(Vector3 world, uint cell)
{
int lbX = (int)(cell >> 24);
return world - new Vector3((lbX - 0xA9) * 192f, 0f, 0f);
}
private static LoadedLandblock EmptyLandblock(uint id) =>
new(id, new LandBlock(), Array.Empty<WorldEntity>());
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell)
{
var position = new CreateObject.ServerPosition(
cell, 191f, 10f, 50f, 1f, 0f, 0f, 0f);
var physics = new PhysicsSpawnData(
(uint)(PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions),
position,
null,
null,
0x02000001u,
0x09000001u,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1));
return new WorldSession.EntitySpawn(
Guid: guid,
Position: position,
SetupTableId: 0x02000001u,
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: 1f,
Name: "remote",
ItemType: null,
MotionState: null,
MotionTableId: 0x09000001u,
PhysicsState: physics.RawState,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private static void CreateUnloadedSourceRollback(RollbackFixture fixture)
{
Assert.True(fixture.ParkPending().Applied);
fixture.Spatial.RemoveLandblock(RollbackFixture.SourceLandblock);
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock));
Assert.True(fixture.Presentation.HasDeferredShadowRestore(RollbackFixture.Guid));
Assert.Equal(0, fixture.ShadowSyncs);
Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered);
}
private static void AcceptNewerPosition(RollbackFixture fixture, uint cellId)
{
Assert.True(fixture.Live.TryApplyPosition(
new WorldSession.EntityPositionUpdate(
RollbackFixture.Guid,
new CreateObject.ServerPosition(
cellId, 1f, 10f, 50f, 1f, 0f, 0f, 0f),
null,
null,
true,
1,
2,
0,
0),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
}
private static void ApplyHidden(RollbackFixture fixture, ushort stateSequence)
{
Assert.True(fixture.Live.TryApplyState(
new SetState.Parsed(
RollbackFixture.Guid,
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
1,
stateSequence),
out _,
out _));
Assert.True(fixture.Presentation.OnStateAccepted(RollbackFixture.Guid));
}
private static void ApplyVisible(RollbackFixture fixture, ushort stateSequence)
{
Assert.True(fixture.Live.TryApplyState(
new SetState.Parsed(
RollbackFixture.Guid,
(uint)PhysicsStateFlags.ReportCollisions,
1,
stateSequence),
out _,
out _));
Assert.True(fixture.Presentation.OnStateAccepted(RollbackFixture.Guid));
}
private sealed class RollbackFixture : IDisposable
{
internal const uint Guid = 0x70000010u;
internal const uint SourceCell = 0xA9B40039u;
internal const uint SourceLandblock = 0xA9B4FFFFu;
internal const uint DestinationCell = 0xAAB40001u;
internal const uint DestinationLandblock = 0xAAB4FFFFu;
internal static readonly Vector3 SourcePosition = new(191f, 10f, 50f);
internal RollbackFixture(bool celllessSource = false)
{
Spatial.AddLandblock(EmptyLandblock(SourceLandblock));
Live = new LiveEntityRuntime(Spatial, new Resources());
Live.RegisterLiveEntity(Spawn(Guid, SourceCell));
Entity = Live.MaterializeLiveEntity(
Guid,
SourceCell,
id => new WorldEntity
{
Id = id,
ServerGuid = Guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = SourcePosition,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
Remote = new GameWindow.RemoteMotion();
Remote.Body.SnapToCell(
celllessSource ? 0u : SourceCell,
SourcePosition,
SourcePosition);
Remote.Body.TransientState = TransientStateFlags.Contact
| TransientStateFlags.OnWalkable;
Remote.CellId = celllessSource ? 0u : SourceCell;
Live.SetRemoteMotionRuntime(Guid, Remote);
Engine.ShadowObjects.Register(
Entity.Id,
Entity.SourceGfxObjOrSetupId,
SourcePosition,
Quaternion.Identity,
0.48f,
0f,
0f,
SourceLandblock,
ShadowCollisionType.Cylinder,
1.835f,
seedCellId: SourceCell,
isStatic: false);
Assert.True(Engine.ShadowObjects.Suspend(Entity.Id));
Live.ProjectionVisibilityChanged += OnVisibilityChanged;
Presentation = new LiveEntityPresentationController(
Live,
Engine.ShadowObjects,
(_, _, _) => true,
liveCenter: () => (0xA9, 0xB4),
onShadowRestored: _ => ShadowSyncs++);
Assert.True(Presentation.OnLiveEntityReady(Guid));
Controller = new RemoteTeleportController(
Engine,
Live,
(_, _) => (0.48f, 1.835f),
CellLocal,
(resolvedEntity, resolvedRemote, resolvedCell) =>
{
ShadowSyncs++;
uint landblock = (resolvedCell & 0xFFFF0000u) | 0xFFFFu;
Engine.ShadowObjects.Register(
resolvedEntity.Id,
resolvedEntity.SourceGfxObjOrSetupId,
resolvedRemote.Position,
resolvedRemote.Orientation,
0.48f,
0f,
0f,
landblock,
ShadowCollisionType.Cylinder,
1.835f,
seedCellId: resolvedCell,
isStatic: false);
},
(guid, generation, deferShadowRestore) =>
Presentation.CompleteAuthoritativePlacement(
guid,
generation,
deferShadowRestore),
(guid, generation) =>
Presentation.BeginAuthoritativePlacement(guid, generation),
resolvePlacement: (position, cell, _, _, _, _) =>
ResolveSucceeds
? new ResolveResult(
position,
cell,
IsOnGround: true,
InContact: true,
OnWalkable: true,
ContactPlane: new Plane(Vector3.UnitZ, 0f),
ContactPlaneCellId: cell)
: new ResolveResult(position, cell, IsOnGround: false, Ok: false));
if (celllessSource)
Assert.True(Live.WithdrawLiveEntityProjection(Guid));
}
internal GpuWorldState Spatial { get; } = new();
internal LiveEntityRuntime Live { get; }
internal WorldEntity Entity { get; }
internal GameWindow.RemoteMotion Remote { get; }
internal PhysicsEngine Engine { get; } = new() { DataCache = new PhysicsDataCache() };
internal RemoteTeleportController Controller { get; }
internal LiveEntityPresentationController Presentation { get; }
internal int ShadowSyncs { get; private set; }
internal bool PresentationVisible { get; private set; } = true;
internal List<bool> VisibilityEdges { get; } = [];
internal bool ResolveSucceeds { get; set; }
internal RemoteTeleportController.Result ParkPending()
{
Controller.BeginPlacement(Guid, generation: 1);
Assert.True(Live.RebucketLiveEntity(Guid, DestinationCell));
return Controller.TryApply(
Remote,
Entity,
new Vector3(193f, 10f, 50f),
DestinationCell,
new Vector3(1f, 10f, 50f),
Quaternion.Identity,
gameTime: 5.0,
destinationProjectionVisible: false,
generation: 1,
positionSequence: 1);
}
public void Dispose()
{
Live.ProjectionVisibilityChanged -= OnVisibilityChanged;
Controller.Dispose();
Presentation.Dispose();
}
private void OnVisibilityChanged(LiveEntityRecord _, bool visible)
{
PresentationVisible = visible;
VisibilityEdges.Add(visible);
}
}
}