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>
This commit is contained in:
parent
31a0889f08
commit
f961d70023
77 changed files with 12513 additions and 1871 deletions
|
|
@ -0,0 +1,835 @@
|
|||
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;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class LiveEntityAnimationSchedulerTests
|
||||
{
|
||||
private const uint LocalGuid = 0x50000001u;
|
||||
private const uint RemoteGuid = 0x70000001u;
|
||||
private const uint Cell = 0x01010001u;
|
||||
|
||||
[Fact]
|
||||
public void HiddenLocalPose_IsSampledOnlyForMarkedObjectQuantum()
|
||||
{
|
||||
var (live, record, animation) = BuildHiddenAnimated(LocalGuid);
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(
|
||||
live,
|
||||
localPlayerGuid: LocalGuid);
|
||||
|
||||
Assert.True(scheduler.Tick(
|
||||
PhysicsBody.MaxQuantum,
|
||||
animation.Entity.Position,
|
||||
localHiddenPartPoseDirty: true,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0)
|
||||
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule marked));
|
||||
bool repeated = scheduler.Tick(
|
||||
PhysicsBody.MaxQuantum,
|
||||
animation.Entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0)
|
||||
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule consumed);
|
||||
|
||||
Assert.True(marked.ComposeParts);
|
||||
Assert.NotNull(marked.SequenceFrames);
|
||||
Assert.False(repeated);
|
||||
Assert.False(consumed.ComposeParts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenRemotePose_IsSampledOnceAfterAdmittedHiddenQuantum()
|
||||
{
|
||||
var (live, record, animation) = BuildHiddenAnimated(RemoteGuid);
|
||||
var remote = new GameWindow.RemoteMotion
|
||||
{
|
||||
CellId = Cell,
|
||||
};
|
||||
remote.Body.Position = animation.Entity.Position;
|
||||
remote.Body.Orientation = animation.Entity.Rotation;
|
||||
remote.Body.InWorld = true;
|
||||
live.SetRemoteMotionRuntime(RemoteGuid, remote);
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(
|
||||
live,
|
||||
localPlayerGuid: LocalGuid);
|
||||
|
||||
Assert.True(scheduler.Tick(
|
||||
PhysicsBody.MaxQuantum,
|
||||
playerPosition: null,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0)
|
||||
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule marked));
|
||||
bool repeated = scheduler.Tick(
|
||||
elapsedSeconds: 0f,
|
||||
playerPosition: null,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0)
|
||||
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule consumed);
|
||||
|
||||
Assert.True(marked.ComposeParts);
|
||||
Assert.NotNull(marked.SequenceFrames);
|
||||
Assert.False(repeated);
|
||||
Assert.False(consumed.ComposeParts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VisibleAnimationWithoutRemoteMotion_AppliesCompleteRootFrame()
|
||||
{
|
||||
var (live, _, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
|
||||
var retainedBodyOwner = BuildRemote(animation.Entity);
|
||||
retainedBodyOwner.Body.SnapToCell(
|
||||
Cell,
|
||||
animation.Entity.Position,
|
||||
animation.Entity.Position);
|
||||
live.SetRemoteMotionRuntime(RemoteGuid, retainedBodyOwner);
|
||||
Assert.True(live.ClearRemoteMotionRuntime(RemoteGuid));
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid);
|
||||
float startX = animation.Entity.Position.X;
|
||||
|
||||
Assert.True(scheduler.Tick(
|
||||
0.1f,
|
||||
animation.Entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0)
|
||||
.ContainsKey(animation.Entity.Id));
|
||||
|
||||
Assert.True(animation.Entity.Position.X > startX + 1f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BodylessNonWalkableAnimation_SuppressesRootOriginButPreservesOrientation()
|
||||
{
|
||||
Quaternion rootTurn = Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitZ,
|
||||
MathF.PI / 2f);
|
||||
var (live, _, animation) = BuildVisibleAnimatedWithPosFrames(
|
||||
RemoteGuid,
|
||||
rootTurn);
|
||||
Vector3 startPosition = animation.Entity.Position;
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid);
|
||||
|
||||
scheduler.Tick(
|
||||
PhysicsBody.MinQuantum * 1.01f,
|
||||
animation.Entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
|
||||
Assert.Equal(startPosition, animation.Entity.Position);
|
||||
Assert.InRange(
|
||||
MathF.Abs(Quaternion.Dot(
|
||||
rootTurn,
|
||||
Quaternion.Normalize(animation.Entity.Rotation))),
|
||||
0.99999f,
|
||||
1.00001f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoteWithoutAnimation_RemainsInOrdinaryObjectWorkset()
|
||||
{
|
||||
var (live, record, entity) = BuildMaterialized(
|
||||
RemoteGuid,
|
||||
PhysicsStateFlags.Gravity);
|
||||
var remote = new GameWindow.RemoteMotion
|
||||
{
|
||||
CellId = Cell,
|
||||
Airborne = true,
|
||||
};
|
||||
remote.Body.Position = entity.Position;
|
||||
remote.Body.Orientation = entity.Rotation;
|
||||
remote.Body.Velocity = new Vector3(3f, 0f, 0f);
|
||||
remote.Body.State = PhysicsStateFlags.Gravity;
|
||||
remote.Body.TransientState = TransientStateFlags.Active;
|
||||
live.SetRemoteMotionRuntime(RemoteGuid, remote);
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid);
|
||||
float startX = entity.Position.X;
|
||||
|
||||
scheduler.Tick(
|
||||
0.1f,
|
||||
entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
|
||||
Assert.True(entity.Position.X > startX);
|
||||
Assert.Same(record, live.SpatialRootObjects[RemoteGuid]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnimationlessRemote_NearPositionCorrectionIsQueuedAndConsumed()
|
||||
{
|
||||
var (live, record, entity) = BuildMaterialized(
|
||||
RemoteGuid,
|
||||
PhysicsStateFlags.ReportCollisions);
|
||||
var remote = BuildRemote(entity);
|
||||
remote.Interp.Enqueue(
|
||||
entity.Position + Vector3.UnitX,
|
||||
heading: 0f,
|
||||
isMovingTo: false,
|
||||
currentBodyPosition: remote.Body.Position);
|
||||
live.SetRemoteMotionRuntime(RemoteGuid, remote);
|
||||
Assert.Null(record.AnimationRuntime);
|
||||
Assert.True(live.IsCurrentSpatialRemoteMotion(record, remote));
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid);
|
||||
float startX = entity.Position.X;
|
||||
|
||||
scheduler.Tick(
|
||||
0.1f,
|
||||
entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1);
|
||||
|
||||
Assert.True(entity.Position.X > startX + 0.5f);
|
||||
Assert.True(remote.Interp.IsActive || entity.Position.X >= startX + 0.99f);
|
||||
Assert.Same(record, live.SpatialRootObjects[RemoteGuid]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetainedProjectileWithoutRemote_WhenMissileClears_AdvancesOnceAsOrdinaryBody()
|
||||
{
|
||||
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(
|
||||
RemoteGuid,
|
||||
rootOrigin: Vector3.Zero);
|
||||
var physics = new PhysicsEngine();
|
||||
var projectiles = new ProjectileController(live, physics);
|
||||
record.FinalPhysicsState = ProjectileState;
|
||||
Assert.True(record.ObjectClock.IsActive);
|
||||
Assert.True(projectiles.TryBind(
|
||||
record,
|
||||
ProjectileSetup(),
|
||||
currentTime: 1.0,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1));
|
||||
Assert.True(record.ObjectClock.IsActive);
|
||||
PhysicsBody body = record.PhysicsBody!;
|
||||
body.set_velocity(new Vector3(3f, 0f, 0f));
|
||||
body.TransientState = TransientStateFlags.Active;
|
||||
|
||||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions
|
||||
| PhysicsStateFlags.Gravity;
|
||||
Assert.True(projectiles.ApplyAuthoritativeState(
|
||||
record,
|
||||
record.FinalPhysicsState,
|
||||
currentTime: 1.1,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1));
|
||||
Assert.True(projectiles.HandlesMovement(RemoteGuid));
|
||||
float startX = animation.Entity.Position.X;
|
||||
int rootPublishes = 0;
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(
|
||||
live,
|
||||
LocalGuid,
|
||||
publishRootPose: _ => rootPublishes++,
|
||||
physics: physics,
|
||||
projectiles: projectiles);
|
||||
|
||||
scheduler.Tick(
|
||||
0.1f,
|
||||
animation.Entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1);
|
||||
|
||||
float distance = animation.Entity.Position.X - startX;
|
||||
Assert.InRange(distance, 0.2f, 0.4f);
|
||||
Assert.Equal(1, rootPublishes);
|
||||
Assert.Same(body, record.PhysicsBody);
|
||||
Assert.Same(body, record.ProjectileRuntime!.Body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetainedProjectileWithRemote_WhenMissileClears_TransfersMovementToRemoteOnce()
|
||||
{
|
||||
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(
|
||||
RemoteGuid,
|
||||
rootOrigin: Vector3.Zero);
|
||||
var physics = new PhysicsEngine();
|
||||
var projectiles = new ProjectileController(live, physics);
|
||||
record.FinalPhysicsState = ProjectileState;
|
||||
var remote = new GameWindow.RemoteMotion
|
||||
{
|
||||
CellId = Cell,
|
||||
Airborne = false,
|
||||
};
|
||||
remote.Body.Position = animation.Entity.Position;
|
||||
remote.Body.Orientation = animation.Entity.Rotation;
|
||||
remote.Body.State = ProjectileState;
|
||||
remote.Body.InWorld = true;
|
||||
remote.Body.TransientState = TransientStateFlags.Active
|
||||
| TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable;
|
||||
live.SetRemoteMotionRuntime(RemoteGuid, remote);
|
||||
Assert.True(record.ObjectClock.IsActive);
|
||||
Assert.Equal(Cell, record.FullCellId);
|
||||
Assert.True(record.IsSpatiallyVisible);
|
||||
Assert.True(projectiles.TryBind(
|
||||
record,
|
||||
ProjectileSetup(),
|
||||
currentTime: 1.0,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1));
|
||||
Assert.Equal(Cell, record.FullCellId);
|
||||
Assert.True(record.IsSpatiallyVisible);
|
||||
Assert.True(record.ObjectClock.IsActive);
|
||||
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
|
||||
|
||||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions
|
||||
| PhysicsStateFlags.Gravity;
|
||||
Assert.True(projectiles.ApplyAuthoritativeState(
|
||||
record,
|
||||
record.FinalPhysicsState,
|
||||
currentTime: 1.1,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1));
|
||||
Assert.False(projectiles.HandlesMovement(RemoteGuid));
|
||||
remote.Interp.Enqueue(
|
||||
animation.Entity.Position + Vector3.UnitX,
|
||||
heading: 0f,
|
||||
isMovingTo: false,
|
||||
currentBodyPosition: remote.Body.Position);
|
||||
Assert.True(record.ObjectClock.IsActive);
|
||||
Assert.True(remote.Body.IsActive);
|
||||
Assert.True(live.IsCurrentSpatialRemoteMotion(record, remote));
|
||||
Assert.True(live.IsCurrentSpatialAnimation(record, animation));
|
||||
float startX = animation.Entity.Position.X;
|
||||
int rootPublishes = 0;
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(
|
||||
live,
|
||||
LocalGuid,
|
||||
publishRootPose: _ => rootPublishes++,
|
||||
physics: physics,
|
||||
projectiles: projectiles);
|
||||
|
||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||
0.1f,
|
||||
animation.Entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1);
|
||||
|
||||
Assert.Contains(animation.Entity.Id, schedules.Keys);
|
||||
Assert.Equal(1, rootPublishes);
|
||||
float distance = animation.Entity.Position.X - startX;
|
||||
Assert.InRange(distance, 0.79f, 0.81f);
|
||||
Assert.Same(remote.Body, record.PhysicsBody);
|
||||
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteFromFirstQuantumCallback_StopsCatchUpBatchAndPosePublish()
|
||||
{
|
||||
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
|
||||
int captures = 0;
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(
|
||||
live,
|
||||
LocalGuid,
|
||||
(_, _) =>
|
||||
{
|
||||
captures++;
|
||||
live.UnregisterLiveEntity(
|
||||
new DeleteObject.Parsed(RemoteGuid, record.Generation),
|
||||
isLocalPlayer: false);
|
||||
});
|
||||
|
||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||
PhysicsBody.MaxQuantum * 2.5f,
|
||||
animation.Entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
|
||||
Assert.Equal(1, captures);
|
||||
Assert.Empty(schedules);
|
||||
Assert.False(live.TryGetRecord(RemoteGuid, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VisibleRemoteDeleteInsideHook_StopsBeforeStaleEntityPublication()
|
||||
{
|
||||
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
|
||||
var remote = BuildRemote(animation.Entity);
|
||||
live.SetRemoteMotionRuntime(RemoteGuid, remote);
|
||||
Vector3 retainedEntityPosition = animation.Entity.Position;
|
||||
int captures = 0;
|
||||
int rootPublishes = 0;
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(
|
||||
live,
|
||||
LocalGuid,
|
||||
(_, _) =>
|
||||
{
|
||||
captures++;
|
||||
live.UnregisterLiveEntity(
|
||||
new DeleteObject.Parsed(RemoteGuid, record.Generation),
|
||||
isLocalPlayer: false);
|
||||
},
|
||||
_ => rootPublishes++);
|
||||
|
||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||
PhysicsBody.MaxQuantum * 2.5f,
|
||||
animation.Entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
|
||||
Assert.Equal(1, captures);
|
||||
Assert.Equal(0, rootPublishes);
|
||||
Assert.Empty(schedules);
|
||||
Assert.Equal(retainedEntityPosition, animation.Entity.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenRemoteDeleteInsideHook_StopsBeforeStaleEntityPublication()
|
||||
{
|
||||
var (live, record, animation) = BuildHiddenAnimated(RemoteGuid);
|
||||
var remote = BuildRemote(animation.Entity);
|
||||
live.SetRemoteMotionRuntime(RemoteGuid, remote);
|
||||
Vector3 retainedEntityPosition = animation.Entity.Position;
|
||||
int captures = 0;
|
||||
int rootPublishes = 0;
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(
|
||||
live,
|
||||
LocalGuid,
|
||||
(_, _) =>
|
||||
{
|
||||
captures++;
|
||||
live.UnregisterLiveEntity(
|
||||
new DeleteObject.Parsed(RemoteGuid, record.Generation),
|
||||
isLocalPlayer: false);
|
||||
},
|
||||
_ => rootPublishes++);
|
||||
|
||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||
PhysicsBody.MaxQuantum * 2.5f,
|
||||
animation.Entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
|
||||
Assert.Equal(1, captures);
|
||||
Assert.Equal(0, rootPublishes);
|
||||
Assert.Empty(schedules);
|
||||
Assert.Equal(retainedEntityPosition, animation.Entity.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenAnimationWithoutRemoteDeleteInsideHook_StopsBeforeManagerTailOrPublication()
|
||||
{
|
||||
var (live, record, animation) = BuildHiddenAnimated(RemoteGuid);
|
||||
int captures = 0;
|
||||
int rootPublishes = 0;
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(
|
||||
live,
|
||||
LocalGuid,
|
||||
(_, _) =>
|
||||
{
|
||||
captures++;
|
||||
live.UnregisterLiveEntity(
|
||||
new DeleteObject.Parsed(RemoteGuid, record.Generation),
|
||||
isLocalPlayer: false);
|
||||
},
|
||||
_ => rootPublishes++);
|
||||
|
||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||
PhysicsBody.MaxQuantum * 2.5f,
|
||||
animation.Entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
|
||||
Assert.Equal(1, captures);
|
||||
Assert.Equal(0, rootPublishes);
|
||||
Assert.Empty(schedules);
|
||||
Assert.False(live.TryGetRecord(RemoteGuid, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithdrawAndReenterInsideHook_InvalidatesOldCatchUpBatch()
|
||||
{
|
||||
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
|
||||
ulong startingEpoch = record.ObjectClockEpoch;
|
||||
int captures = 0;
|
||||
int rootPublishes = 0;
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(
|
||||
live,
|
||||
LocalGuid,
|
||||
(_, _) =>
|
||||
{
|
||||
captures++;
|
||||
Assert.True(live.WithdrawLiveEntityProjection(RemoteGuid));
|
||||
Assert.Same(
|
||||
animation.Entity,
|
||||
live.MaterializeLiveEntity(
|
||||
RemoteGuid,
|
||||
Cell,
|
||||
_ => throw new InvalidOperationException(
|
||||
"A retained projection must not be reconstructed.")));
|
||||
},
|
||||
_ => rootPublishes++);
|
||||
|
||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||
PhysicsBody.MaxQuantum * 2.5f,
|
||||
animation.Entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
|
||||
Assert.Equal(1, captures);
|
||||
Assert.Equal(0, rootPublishes);
|
||||
Assert.Empty(schedules);
|
||||
Assert.Equal(startingEpoch + 2, record.ObjectClockEpoch);
|
||||
Assert.True(record.ObjectClock.IsActive);
|
||||
Assert.Equal(0d, record.ObjectClock.PendingSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LocalRootPosePublishesWithoutAnimationAndBetweenObjectQuanta()
|
||||
{
|
||||
var (live, _, entity) = BuildMaterialized(LocalGuid, PhysicsStateFlags.None);
|
||||
var published = new List<Vector3>();
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(
|
||||
live,
|
||||
LocalGuid,
|
||||
publishRootPose: current => published.Add(current.Position));
|
||||
|
||||
scheduler.Tick(
|
||||
PhysicsBody.MinQuantum * 0.25f,
|
||||
entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
entity.SetPosition(entity.Position + new Vector3(0.25f, 0f, 0f));
|
||||
scheduler.Tick(
|
||||
PhysicsBody.MinQuantum * 0.25f,
|
||||
entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
|
||||
Assert.Equal(2, published.Count);
|
||||
Assert.Equal(entity.Position, published[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttachedProjection_IsNotAnIndependentOrdinaryRoot()
|
||||
{
|
||||
var (live, record, entity) = BuildMaterialized(
|
||||
RemoteGuid,
|
||||
PhysicsStateFlags.None);
|
||||
int rootPublishes = 0;
|
||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(
|
||||
live,
|
||||
LocalGuid,
|
||||
publishRootPose: _ => rootPublishes++);
|
||||
|
||||
Assert.Same(
|
||||
entity,
|
||||
live.MaterializeLiveEntity(
|
||||
RemoteGuid,
|
||||
Cell,
|
||||
_ => throw new InvalidOperationException(),
|
||||
LiveEntityProjectionKind.Attached));
|
||||
Assert.False(record.ObjectClock.IsActive);
|
||||
Assert.DoesNotContain(RemoteGuid, live.SpatialRootObjects.Keys);
|
||||
|
||||
scheduler.Tick(
|
||||
PhysicsBody.MaxQuantum,
|
||||
entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
Assert.Equal(0, rootPublishes);
|
||||
|
||||
Assert.Same(
|
||||
entity,
|
||||
live.MaterializeLiveEntity(
|
||||
RemoteGuid,
|
||||
Cell,
|
||||
_ => throw new InvalidOperationException(),
|
||||
LiveEntityProjectionKind.World));
|
||||
Assert.True(record.ObjectClock.IsActive);
|
||||
Assert.Contains(RemoteGuid, live.SpatialRootObjects.Keys);
|
||||
|
||||
scheduler.Tick(
|
||||
PhysicsBody.MaxQuantum,
|
||||
entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
Assert.Equal(1, rootPublishes);
|
||||
}
|
||||
|
||||
private static GameWindow.RemoteMotion BuildRemote(WorldEntity entity)
|
||||
{
|
||||
var remote = new GameWindow.RemoteMotion
|
||||
{
|
||||
CellId = Cell,
|
||||
};
|
||||
remote.Body.Position = entity.Position;
|
||||
remote.Body.Orientation = entity.Rotation;
|
||||
remote.Body.InWorld = true;
|
||||
remote.Body.TransientState = TransientStateFlags.Active
|
||||
| TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable;
|
||||
return remote;
|
||||
}
|
||||
|
||||
private static LiveEntityAnimationScheduler BuildScheduler(
|
||||
LiveEntityRuntime live,
|
||||
uint localPlayerGuid,
|
||||
Action<uint, AnimationSequencer>? captureHooks = null,
|
||||
Action<WorldEntity>? publishRootPose = null,
|
||||
PhysicsEngine? physics = null,
|
||||
ProjectileController? projectiles = null)
|
||||
{
|
||||
physics ??= new PhysicsEngine();
|
||||
var remotePhysics = new RemotePhysicsUpdater(
|
||||
physics,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
var ordinaryPhysics = new LiveEntityOrdinaryPhysicsUpdater(
|
||||
physics,
|
||||
(_, _) => (0.48f, 1.835f));
|
||||
return new LiveEntityAnimationScheduler(
|
||||
() => live,
|
||||
() => localPlayerGuid,
|
||||
remotePhysics,
|
||||
ordinaryPhysics,
|
||||
() => projectiles,
|
||||
publishRootPose ?? (_ => { }),
|
||||
captureHooks ?? ((_, _) => { }));
|
||||
}
|
||||
|
||||
private static (LiveEntityRuntime Live, LiveEntityRecord Record,
|
||||
GameWindow.AnimatedEntity Animation) BuildHiddenAnimated(uint guid)
|
||||
{
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(new LoadedLandblock(
|
||||
0x0101FFFFu,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
var live = new LiveEntityRuntime(
|
||||
spatial,
|
||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||
LiveEntityRecord record = live.RegisterLiveEntity(Spawn(guid)).Record!;
|
||||
WorldEntity entity = live.MaterializeLiveEntity(
|
||||
guid,
|
||||
Cell,
|
||||
id => new WorldEntity
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = guid,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = new Vector3(10f, 10f, 5f),
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
ParentCellId = Cell,
|
||||
})!;
|
||||
var setup = new Setup();
|
||||
var animation = new GameWindow.AnimatedEntity
|
||||
{
|
||||
Entity = entity,
|
||||
Setup = setup,
|
||||
Animation = new Animation(),
|
||||
LowFrame = 0,
|
||||
HighFrame = 0,
|
||||
Framerate = 0f,
|
||||
Scale = 1f,
|
||||
PartTemplate = Array.Empty<GameWindow.AnimatedPartTemplate>(),
|
||||
PartAvailability = Array.Empty<bool>(),
|
||||
Sequencer = new AnimationSequencer(
|
||||
setup,
|
||||
new MotionTable(),
|
||||
new NullAnimationLoader()),
|
||||
};
|
||||
record.HasPartArray = true;
|
||||
live.SetAnimationRuntime(guid, animation);
|
||||
return (live, record, animation);
|
||||
}
|
||||
|
||||
private static (LiveEntityRuntime Live, LiveEntityRecord Record,
|
||||
GameWindow.AnimatedEntity Animation) BuildVisibleAnimatedWithPosFrames(
|
||||
uint guid,
|
||||
Quaternion? rootOrientation = null,
|
||||
Vector3? rootOrigin = null)
|
||||
{
|
||||
var (live, record, entity) = BuildMaterialized(guid, PhysicsStateFlags.None);
|
||||
const uint animationId = 0x0300AA01u;
|
||||
var setup = new Setup();
|
||||
setup.Parts.Add(0x0100AA01u);
|
||||
setup.DefaultScale.Add(Vector3.One);
|
||||
var datAnimation = new Animation
|
||||
{
|
||||
Flags = AnimationFlags.PosFrames,
|
||||
};
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var partFrame = new AnimationFrame(1);
|
||||
partFrame.Frames.Add(new Frame
|
||||
{
|
||||
Origin = Vector3.Zero,
|
||||
Orientation = Quaternion.Identity,
|
||||
});
|
||||
datAnimation.PartFrames.Add(partFrame);
|
||||
datAnimation.PosFrames.Add(new Frame
|
||||
{
|
||||
Origin = rootOrigin ?? Vector3.UnitX,
|
||||
Orientation = rootOrientation ?? Quaternion.Identity,
|
||||
});
|
||||
}
|
||||
var loader = new DictionaryAnimationLoader(animationId, datAnimation);
|
||||
var sequencer = new AnimationSequencer(setup, new MotionTable(), loader);
|
||||
Assert.True(sequencer.InitializeSetupDefaultAnimation(animationId));
|
||||
var animation = new GameWindow.AnimatedEntity
|
||||
{
|
||||
Entity = entity,
|
||||
Setup = setup,
|
||||
Animation = datAnimation,
|
||||
LowFrame = 0,
|
||||
HighFrame = 3,
|
||||
Framerate = 30f,
|
||||
Scale = 1f,
|
||||
PartTemplate = new[]
|
||||
{
|
||||
new GameWindow.AnimatedPartTemplate(
|
||||
0x0100AA01u,
|
||||
SurfaceOverrides: null,
|
||||
IsDrawable: true),
|
||||
},
|
||||
PartAvailability = new[] { true },
|
||||
Sequencer = sequencer,
|
||||
};
|
||||
record.HasPartArray = true;
|
||||
live.SetAnimationRuntime(guid, animation);
|
||||
return (live, record, animation);
|
||||
}
|
||||
|
||||
private static readonly PhysicsStateFlags ProjectileState =
|
||||
PhysicsStateFlags.ReportCollisions
|
||||
| PhysicsStateFlags.Missile
|
||||
| PhysicsStateFlags.AlignPath
|
||||
| PhysicsStateFlags.PathClipped;
|
||||
|
||||
private static Setup ProjectileSetup() => new()
|
||||
{
|
||||
Spheres =
|
||||
{
|
||||
new Sphere
|
||||
{
|
||||
Origin = Vector3.Zero,
|
||||
Radius = 0.1f,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
private static (LiveEntityRuntime Live, LiveEntityRecord Record, WorldEntity Entity)
|
||||
BuildMaterialized(uint guid, PhysicsStateFlags state)
|
||||
{
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(new LoadedLandblock(
|
||||
0x0101FFFFu,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
var live = new LiveEntityRuntime(
|
||||
spatial,
|
||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||
LiveEntityRecord record = live.RegisterLiveEntity(Spawn(guid, state)).Record!;
|
||||
WorldEntity entity = live.MaterializeLiveEntity(
|
||||
guid,
|
||||
Cell,
|
||||
id => new WorldEntity
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = guid,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = new Vector3(10f, 10f, 5f),
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
ParentCellId = Cell,
|
||||
})!;
|
||||
return (live, record, entity);
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(
|
||||
uint guid,
|
||||
PhysicsStateFlags state = PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.IgnoreCollisions)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
Cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
|
||||
var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)state,
|
||||
Position: position,
|
||||
Movement: null,
|
||||
AnimationFrame: null,
|
||||
SetupTableId: 0x02000001u,
|
||||
MotionTableId: 0x09000001u,
|
||||
SoundTableId: null,
|
||||
PhysicsScriptTableId: null,
|
||||
Parent: null,
|
||||
Children: null,
|
||||
Scale: null,
|
||||
Friction: null,
|
||||
Elasticity: null,
|
||||
Translucency: null,
|
||||
Velocity: null,
|
||||
Acceleration: null,
|
||||
AngularVelocity: null,
|
||||
DefaultScriptType: null,
|
||||
DefaultScriptIntensity: null,
|
||||
Timestamps: timestamps);
|
||||
return new WorldSession.EntitySpawn(
|
||||
guid,
|
||||
position,
|
||||
0x02000001u,
|
||||
Array.Empty<CreateObject.AnimPartChange>(),
|
||||
Array.Empty<CreateObject.TextureChange>(),
|
||||
Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||
null,
|
||||
null,
|
||||
"hidden scheduler fixture",
|
||||
null,
|
||||
null,
|
||||
0x09000001u,
|
||||
PhysicsState: (uint)state,
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
|
||||
private sealed class NullAnimationLoader : IAnimationLoader
|
||||
{
|
||||
public Animation? LoadAnimation(uint id) => null;
|
||||
}
|
||||
|
||||
private sealed class DictionaryAnimationLoader : IAnimationLoader
|
||||
{
|
||||
private readonly uint _id;
|
||||
private readonly Animation _animation;
|
||||
|
||||
public DictionaryAnimationLoader(uint id, Animation animation)
|
||||
{
|
||||
_id = id;
|
||||
_animation = animation;
|
||||
}
|
||||
|
||||
public Animation? LoadAnimation(uint id) => id == _id ? _animation : null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue