acdream/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationSchedulerTests.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

835 lines
29 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;
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;
}
}