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>
1518 lines
59 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|