refactor(runtime): own per-session physics simulation
Move the sole PhysicsEngine, production cache, collision admissions, canonical bodies and hosts, remote components, ordinary/remote worksets, simulation, cell commits, and shadow synchronization under RuntimeEntityObjectLifetime. Keep App as the prepared-asset, animation-input, and render-projection adapter while preserving the named-retail update and collision order. Add exact-incarnation, object-clock, callback-reentrancy, GUID-reuse, two-runtime isolation, source ownership, collision publication, and graphical projection coverage. Release build and the complete 8,588-test solution pass. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
0dc3bfdeff
commit
7e6033d0ad
39 changed files with 3685 additions and 1722 deletions
|
|
@ -1,4 +1,15 @@
|
|||
global using AcDream.Runtime.Gameplay;
|
||||
global using AcDream.Runtime.Physics;
|
||||
global using ILiveEntityRemoteMotionRuntime =
|
||||
AcDream.Runtime.Physics.IRuntimeRemoteMotion;
|
||||
global using ILiveEntityPhysicsHostConsumer =
|
||||
AcDream.Runtime.Physics.IRuntimePhysicsHostConsumer;
|
||||
global using ILiveEntityCanonicalRuntimeConsumer =
|
||||
AcDream.Runtime.Physics.IRuntimeCanonicalPhysicsConsumer;
|
||||
global using ILiveEntityCanonicalCellConsumer =
|
||||
AcDream.Runtime.Physics.IRuntimeCanonicalCellConsumer;
|
||||
global using ILiveEntityRemotePlacementRuntime =
|
||||
AcDream.Runtime.Physics.IRuntimeRemotePlacement;
|
||||
global using LocalPlayerControllerSlot =
|
||||
AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState;
|
||||
global using ILocalPlayerControllerSource =
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
|||
[Fact]
|
||||
public void CompleteRootFrame_CrossesTransitionAndCommitsCanonicalCell()
|
||||
{
|
||||
PhysicsEngine physics = BuildBoundaryEngine();
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu));
|
||||
spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu));
|
||||
var live = LiveEntityRuntimeFixture.Create(
|
||||
spatial,
|
||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||
PopulateBoundaryEngine(live.Physics.Engine);
|
||||
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
|
||||
Spawn(),
|
||||
id => new WorldEntity
|
||||
|
|
@ -40,7 +40,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
|||
});
|
||||
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||
|
||||
var remote = new AcDream.App.Physics.RemoteMotion
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion
|
||||
{
|
||||
CellId = SourceCell,
|
||||
};
|
||||
|
|
@ -56,7 +56,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
|||
ulong epoch = record.ObjectClockEpoch;
|
||||
|
||||
var updater = new LiveEntityOrdinaryPhysicsUpdater(
|
||||
physics,
|
||||
live.Physics,
|
||||
(_, _) => (0.48f, 1.835f));
|
||||
var rootFrame = new Frame
|
||||
{
|
||||
|
|
@ -106,7 +106,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
|||
ParentCellId = SourceCell,
|
||||
});
|
||||
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||
var remote = new AcDream.App.Physics.RemoteMotion
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion
|
||||
{
|
||||
CellId = SourceCell,
|
||||
};
|
||||
|
|
@ -123,7 +123,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
|||
new NullAnimationLoader());
|
||||
Vector3 retainedEntityPosition = entity.Position;
|
||||
var updater = new LiveEntityOrdinaryPhysicsUpdater(
|
||||
new PhysicsEngine(),
|
||||
live.Physics,
|
||||
(_, _) => (0.48f, 1.835f));
|
||||
|
||||
Assert.False(updater.Tick(
|
||||
|
|
@ -149,7 +149,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
|||
Assert.Equal(retainedEntityPosition, entity.Position);
|
||||
}
|
||||
|
||||
private static PhysicsEngine BuildBoundaryEngine()
|
||||
private static void PopulateBoundaryEngine(PhysicsEngine engine)
|
||||
{
|
||||
static TerrainSurface FlatTerrain()
|
||||
{
|
||||
|
|
@ -159,7 +159,6 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
|||
return new TerrainSurface(heights, table);
|
||||
}
|
||||
|
||||
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
|
||||
engine.AddLandblock(
|
||||
0xA9B4FFFFu,
|
||||
FlatTerrain(),
|
||||
|
|
@ -174,7 +173,6 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
|||
Array.Empty<PortalPlane>(),
|
||||
192f,
|
||||
0f);
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static LoadedLandblock EmptyLandblock(uint landblockId) =>
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ public sealed class ProjectileControllerTests
|
|||
LiveEntityRecord record = fixture.Spawn(instance: 1);
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
Assert.Null(record.AnimationRuntime);
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
|
||||
remote.Body.Position = entity.Position;
|
||||
remote.Body.Orientation = entity.Rotation;
|
||||
|
|
@ -124,7 +124,7 @@ public sealed class ProjectileControllerTests
|
|||
isMovingTo: false,
|
||||
currentBodyPosition: remote.Body.Position);
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
fixture.Engine,
|
||||
fixture.Live.Physics,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
|
||||
|
|
@ -685,7 +685,7 @@ public sealed class ProjectileControllerTests
|
|||
LiveEntityRecord record = fixture.Spawn(instance: 1);
|
||||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
|
||||
PhysicsBody body = record.PhysicsBody!;
|
||||
fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.App.Physics.RemoteMotion(body));
|
||||
fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.Runtime.Physics.RemoteMotion(body));
|
||||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
||||
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
||||
record, record.FinalPhysicsState, 2.0, 1, 1));
|
||||
|
|
@ -709,7 +709,7 @@ public sealed class ProjectileControllerTests
|
|||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
|
||||
PhysicsBody body = record.PhysicsBody!;
|
||||
Vector3 position = body.Position;
|
||||
fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.App.Physics.RemoteMotion(body));
|
||||
fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.Runtime.Physics.RemoteMotion(body));
|
||||
|
||||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
||||
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
||||
|
|
@ -948,7 +948,7 @@ public sealed class ProjectileControllerTests
|
|||
var fixture = new Fixture();
|
||||
LiveEntityRecord record = fixture.Spawn(instance: 1);
|
||||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
|
||||
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
|
||||
|
||||
|
|
@ -1000,7 +1000,7 @@ public sealed class ProjectileControllerTests
|
|||
var fixture = new Fixture();
|
||||
LiveEntityRecord record = fixture.Spawn(instance: 1);
|
||||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
|
||||
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
|
||||
|
||||
|
|
@ -1072,7 +1072,7 @@ public sealed class ProjectileControllerTests
|
|||
seedCellId: startCell,
|
||||
isStatic: false);
|
||||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
|
||||
Assert.Equal(new Vector3(40f, 0f, 0f), remote.Body.Velocity);
|
||||
Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega);
|
||||
|
|
@ -1138,7 +1138,7 @@ public sealed class ProjectileControllerTests
|
|||
velocity: new Vector3(float.NaN, 0f, 0f));
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
|
||||
remote.Body.Position = entity.Position;
|
||||
remote.Body.Orientation = entity.Rotation;
|
||||
|
|
@ -1164,7 +1164,7 @@ public sealed class ProjectileControllerTests
|
|||
LiveEntityRecord record = fixture.Spawn(instance: 1);
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
|
||||
remote.Body.Position = entity.Position;
|
||||
remote.Body.Orientation = entity.Rotation;
|
||||
|
|
@ -1188,7 +1188,7 @@ public sealed class ProjectileControllerTests
|
|||
LiveEntityRecord record = fixture.Spawn(instance: 1);
|
||||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
|
||||
PhysicsBody body = record.PhysicsBody!;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion(body);
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion(body);
|
||||
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
|
||||
|
||||
fixture.Live.RebucketLiveEntity(Guid, CellB);
|
||||
|
|
@ -1200,7 +1200,7 @@ public sealed class ProjectileControllerTests
|
|||
Assert.Same(body, record.RemoteMotionRuntime!.Body);
|
||||
Assert.Same(body, record.ProjectileRuntime!.Body);
|
||||
|
||||
var replacement = new AcDream.App.Physics.RemoteMotion(body);
|
||||
var replacement = new AcDream.Runtime.Physics.RemoteMotion(body);
|
||||
fixture.Live.SetRemoteMotionRuntime(Guid, replacement);
|
||||
remote.CellId = CellB;
|
||||
Assert.Equal(CellA, record.FullCellId);
|
||||
|
|
|
|||
|
|
@ -57,10 +57,9 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Quaternion turn = Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitZ,
|
||||
MathF.PI / 2f);
|
||||
var engine = new PhysicsEngine();
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 13u,
|
||||
Id = LiveEntityRuntime.FirstLiveEntityId + 13u,
|
||||
ServerGuid = 0x80000013u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
|
|
@ -68,7 +67,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
ParentCellId = cellId,
|
||||
};
|
||||
var motion = new AcDream.App.Physics.RemoteMotion
|
||||
var motion = new AcDream.Runtime.Physics.RemoteMotion
|
||||
{
|
||||
LastShadowSyncPos = Vector3.Zero,
|
||||
LastShadowSyncOrientation = Quaternion.Identity,
|
||||
|
|
@ -76,7 +75,8 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
motion.Body.Position = Vector3.Zero;
|
||||
motion.Body.Orientation = Quaternion.Identity;
|
||||
motion.CellId = cellId;
|
||||
engine.ShadowObjects.RegisterMultiPart(
|
||||
RemoteBinding binding = BindRemote(entity, motion, cellId);
|
||||
binding.Engine.ShadowObjects.RegisterMultiPart(
|
||||
entity.Id,
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
|
|
@ -96,12 +96,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
worldOffsetY: 0f,
|
||||
landblockId: 0x01010000u,
|
||||
seedCellId: cellId);
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
engine,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
|
||||
updater.Tick(
|
||||
binding.Updater.Tick(
|
||||
motion,
|
||||
entity,
|
||||
objectScale: 1f,
|
||||
|
|
@ -110,10 +105,13 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
dt: 0.1f,
|
||||
new MotionDeltaFrame { Orientation = turn },
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1);
|
||||
liveCenterY: 1,
|
||||
ownerRuntime: binding.Live,
|
||||
ownerRecord: binding.Record,
|
||||
ownerClockEpoch: binding.Record.ObjectClockEpoch);
|
||||
|
||||
ShadowEntry entry = Assert.Single(
|
||||
engine.ShadowObjects.AllEntriesForDebug(),
|
||||
binding.Engine.ShadowObjects.AllEntriesForDebug(),
|
||||
candidate => candidate.EntityId == entity.Id);
|
||||
Assert.Equal(0f, entry.Position.X, 3);
|
||||
Assert.Equal(1f, entry.Position.Y, 3);
|
||||
|
|
@ -130,7 +128,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
{
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 10u,
|
||||
Id = LiveEntityRuntime.FirstLiveEntityId + 10u,
|
||||
ServerGuid = 0x80000010u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = new Vector3(10f, 20f, 30f),
|
||||
|
|
@ -151,7 +149,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
|
||||
PartAvailability = Array.Empty<bool>(),
|
||||
};
|
||||
var motion = new AcDream.App.Physics.RemoteMotion();
|
||||
var motion = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
motion.Body.Position = entity.Position;
|
||||
motion.Body.Orientation = entity.Rotation;
|
||||
motion.Body.TransientState = TransientStateFlags.Contact
|
||||
|
|
@ -164,18 +162,17 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Vector3.UnitZ,
|
||||
MathF.PI / 2f),
|
||||
};
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
new PhysicsEngine(),
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
|
||||
updater.Tick(
|
||||
RemoteBinding binding = BindRemote(entity, motion);
|
||||
binding.Updater.Tick(
|
||||
motion,
|
||||
animated,
|
||||
dt: 0.1f,
|
||||
sequenceFrame,
|
||||
liveCenterX: 0,
|
||||
liveCenterY: 0);
|
||||
liveCenterY: 0,
|
||||
ownerRuntime: binding.Live,
|
||||
ownerRecord: binding.Record,
|
||||
ownerClockEpoch: binding.Record.ObjectClockEpoch);
|
||||
|
||||
// Current body faces east, so the sequence's local +Y origin moves
|
||||
// east before its quarter-turn is composed. No ObservedOmega/manual
|
||||
|
|
@ -198,7 +195,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Quaternion rootTurn = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -0.9f);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 11u,
|
||||
Id = LiveEntityRuntime.FirstLiveEntityId + 11u,
|
||||
ServerGuid = 0x80000011u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = new Vector3(10f, 20f, 30f),
|
||||
|
|
@ -217,7 +214,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
|
||||
PartAvailability = Array.Empty<bool>(),
|
||||
};
|
||||
var motion = new AcDream.App.Physics.RemoteMotion
|
||||
var motion = new AcDream.Runtime.Physics.RemoteMotion
|
||||
{
|
||||
Airborne = true,
|
||||
};
|
||||
|
|
@ -229,12 +226,17 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Origin = new Vector3(0f, 10f, 0f),
|
||||
Orientation = rootTurn,
|
||||
};
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
new PhysicsEngine(),
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
|
||||
updater.Tick(motion, animated, 0.1f, root, 0, 0);
|
||||
RemoteBinding binding = BindRemote(entity, motion);
|
||||
binding.Updater.Tick(
|
||||
motion,
|
||||
animated,
|
||||
0.1f,
|
||||
root,
|
||||
0,
|
||||
0,
|
||||
ownerRuntime: binding.Live,
|
||||
ownerRecord: binding.Record,
|
||||
ownerClockEpoch: binding.Record.ObjectClockEpoch);
|
||||
|
||||
Assert.Equal(new Vector3(10f, 20f, 30f), motion.Body.Position);
|
||||
Quaternion expected = Quaternion.Normalize(initial * rootTurn);
|
||||
|
|
@ -253,7 +255,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Quaternion target = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -0.9f);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 12u,
|
||||
Id = LiveEntityRuntime.FirstLiveEntityId + 12u,
|
||||
ServerGuid = 0x80000012u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = new Vector3(10f, 20f, 30f),
|
||||
|
|
@ -272,7 +274,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
|
||||
PartAvailability = Array.Empty<bool>(),
|
||||
};
|
||||
var motion = new AcDream.App.Physics.RemoteMotion();
|
||||
var motion = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
motion.Body.Position = entity.Position;
|
||||
motion.Body.Orientation = initial;
|
||||
motion.Interp.Enqueue(
|
||||
|
|
@ -285,12 +287,17 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Origin = new Vector3(0f, 10f, 0f),
|
||||
Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.7f),
|
||||
};
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
new PhysicsEngine(),
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
|
||||
updater.Tick(motion, animated, 0.1f, root, 0, 0);
|
||||
RemoteBinding binding = BindRemote(entity, motion);
|
||||
binding.Updater.Tick(
|
||||
motion,
|
||||
animated,
|
||||
0.1f,
|
||||
root,
|
||||
0,
|
||||
0,
|
||||
ownerRuntime: binding.Live,
|
||||
ownerRecord: binding.Record,
|
||||
ownerClockEpoch: binding.Record.ObjectClockEpoch);
|
||||
|
||||
// InterpolationManager::adjust_offset clamps the 1 m correction to
|
||||
// maxSpeed (8 m/s) * 0.1 s = 0.8 m and replaces the authored 10 m
|
||||
|
|
@ -308,7 +315,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
[Fact]
|
||||
public void TickHidden_AppliesPositionManagerOffsetWithoutAdvancingPhysics()
|
||||
{
|
||||
var motion = new AcDream.App.Physics.RemoteMotion();
|
||||
var motion = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
motion.Body.Position = Vector3.Zero;
|
||||
motion.Body.Orientation = Quaternion.Identity;
|
||||
motion.Body.Velocity = new Vector3(4f, 0f, 5f);
|
||||
|
|
@ -321,7 +328,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
currentBodyPosition: Vector3.Zero);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 1u,
|
||||
Id = LiveEntityRuntime.FirstLiveEntityId + 1u,
|
||||
ServerGuid = 0x70000001u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
|
|
@ -329,13 +336,19 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
new PhysicsEngine(),
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
RemoteBinding binding = BindRemote(
|
||||
entity,
|
||||
motion,
|
||||
motion.CellId);
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
poses.Publish(entity, Array.Empty<Matrix4x4>());
|
||||
updater.TickHidden(motion, entity, 0.1f);
|
||||
binding.Updater.TickHidden(
|
||||
motion,
|
||||
entity,
|
||||
0.1f,
|
||||
ownerRuntime: binding.Live,
|
||||
ownerRecord: binding.Record,
|
||||
ownerClockEpoch: binding.Record.ObjectClockEpoch);
|
||||
Assert.True(poses.UpdateRoot(entity));
|
||||
|
||||
Assert.InRange(motion.Body.Position.X, 0.01f, 1f);
|
||||
|
|
@ -350,29 +363,29 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
[Fact]
|
||||
public void TickHidden_RunsPartArrayManagerTailWithoutAdvancingSequence()
|
||||
{
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
new PhysicsEngine(),
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
var motion = new AcDream.App.Physics.RemoteMotion();
|
||||
var motion = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
motion.Body.Orientation = Quaternion.Identity;
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 2u,
|
||||
Id = LiveEntityRuntime.FirstLiveEntityId + 2u,
|
||||
ServerGuid = 0x70000002u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
RemoteBinding binding = BindRemote(entity, motion);
|
||||
AnimationSequencer sequencer = CreateSequencer();
|
||||
sequencer.Manager.AddToQueue(MotionTableManager.ReadySentinel, 0);
|
||||
|
||||
updater.TickHidden(
|
||||
binding.Updater.TickHidden(
|
||||
motion,
|
||||
entity,
|
||||
0.1f,
|
||||
sequencer.Manager);
|
||||
sequencer.Manager,
|
||||
ownerRuntime: binding.Live,
|
||||
ownerRecord: binding.Record,
|
||||
ownerClockEpoch: binding.Record.ObjectClockEpoch);
|
||||
|
||||
Assert.Empty(sequencer.Manager.PendingAnimations);
|
||||
}
|
||||
|
|
@ -380,26 +393,23 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
[Fact]
|
||||
public void TickHidden_ProcessesPendingHooksBeforeManagerTail()
|
||||
{
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
new PhysicsEngine(),
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
var motion = new AcDream.App.Physics.RemoteMotion();
|
||||
var motion = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
motion.Body.Orientation = Quaternion.Identity;
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 3u,
|
||||
Id = LiveEntityRuntime.FirstLiveEntityId + 3u,
|
||||
ServerGuid = 0x70000003u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
RemoteBinding binding = BindRemote(entity, motion);
|
||||
AnimationSequencer sequencer = CreateSequencer();
|
||||
sequencer.Manager.AddToQueue(MotionTableManager.ReadySentinel, 0);
|
||||
var order = new List<string>();
|
||||
|
||||
updater.TickHidden(
|
||||
binding.Updater.TickHidden(
|
||||
motion,
|
||||
entity,
|
||||
0.1f,
|
||||
|
|
@ -410,7 +420,10 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Assert.NotEmpty(sequencer.Manager.PendingAnimations);
|
||||
order.Add("hooks");
|
||||
},
|
||||
sequencer);
|
||||
sequencer,
|
||||
binding.Live,
|
||||
binding.Record,
|
||||
binding.Record.ObjectClockEpoch);
|
||||
|
||||
Assert.Equal(["hooks"], order);
|
||||
Assert.Empty(sequencer.Manager.PendingAnimations);
|
||||
|
|
@ -419,27 +432,13 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
[Fact]
|
||||
public void TickHidden_CrossCellCommitUpdatesCanonicalCellBeforeUnhide()
|
||||
{
|
||||
var engine = BuildBoundaryEngine();
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
engine,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
var motion = new AcDream.App.Physics.RemoteMotion();
|
||||
var motion = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
motion.Body.Position = new Vector3(191f, 10f, 50f);
|
||||
motion.Body.Orientation = Quaternion.Identity;
|
||||
motion.Body.TransientState = TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable;
|
||||
uint canonicalCell = 0xA9B40039u;
|
||||
int canonicalWrites = 0;
|
||||
motion.BindCanonicalCell(
|
||||
() => canonicalCell,
|
||||
value =>
|
||||
{
|
||||
canonicalCell = value;
|
||||
canonicalWrites++;
|
||||
});
|
||||
motion.CellId = canonicalCell;
|
||||
canonicalWrites = 0;
|
||||
motion.Interp.Enqueue(
|
||||
new Vector3(193f, 10f, 50f),
|
||||
heading: 0f,
|
||||
|
|
@ -447,31 +446,41 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
currentBodyPosition: motion.Body.Position);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 1u,
|
||||
Id = LiveEntityRuntime.FirstLiveEntityId + 1u,
|
||||
ServerGuid = 0x70000002u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = motion.Body.Position,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
RemoteBinding binding = BindRemote(
|
||||
entity,
|
||||
motion,
|
||||
canonicalCell);
|
||||
PopulateBoundaryEngine(binding.Engine);
|
||||
ulong spatialAuthority = binding.Record.Canonical
|
||||
.SpatialAuthorityVersion;
|
||||
|
||||
updater.TickHidden(motion, entity, 2f);
|
||||
binding.Updater.TickHidden(
|
||||
motion,
|
||||
entity,
|
||||
2f,
|
||||
ownerRuntime: binding.Live,
|
||||
ownerRecord: binding.Record,
|
||||
ownerClockEpoch: binding.Record.ObjectClockEpoch);
|
||||
|
||||
Assert.Equal(0xAAB40001u, canonicalCell);
|
||||
Assert.Equal(canonicalCell, entity.ParentCellId);
|
||||
Assert.True(canonicalWrites > 0);
|
||||
Assert.Equal(0xAAB40001u, binding.Record.FullCellId);
|
||||
Assert.Equal(binding.Record.FullCellId, entity.ParentCellId);
|
||||
Assert.True(
|
||||
binding.Record.Canonical.SpatialAuthorityVersion
|
||||
> spatialAuthority);
|
||||
Assert.True(entity.Position.X > 192f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TickHidden_OutOfContactDoesNotInterpolateOrInventLanding()
|
||||
{
|
||||
var engine = BuildBoundaryEngine();
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
engine,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
var motion = new AcDream.App.Physics.RemoteMotion
|
||||
var motion = new AcDream.Runtime.Physics.RemoteMotion
|
||||
{
|
||||
Airborne = true,
|
||||
};
|
||||
|
|
@ -486,15 +495,26 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
currentBodyPosition: motion.Body.Position);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 3u,
|
||||
Id = LiveEntityRuntime.FirstLiveEntityId + 3u,
|
||||
ServerGuid = 0x70000003u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = motion.Body.Position,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
RemoteBinding binding = BindRemote(
|
||||
entity,
|
||||
motion,
|
||||
motion.CellId);
|
||||
PopulateBoundaryEngine(binding.Engine);
|
||||
|
||||
updater.TickHidden(motion, entity, 0.1f);
|
||||
binding.Updater.TickHidden(
|
||||
motion,
|
||||
entity,
|
||||
0.1f,
|
||||
ownerRuntime: binding.Live,
|
||||
ownerRecord: binding.Record,
|
||||
ownerClockEpoch: binding.Record.ObjectClockEpoch);
|
||||
|
||||
Assert.False(motion.Body.InContact);
|
||||
Assert.False(motion.Body.OnWalkable);
|
||||
|
|
@ -591,7 +611,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
using var fixture = new BoundaryRemoteFixture();
|
||||
LiveEntityRecord oldRecord = fixture.Record;
|
||||
WorldEntity oldEntity = fixture.Entity;
|
||||
AcDream.App.Physics.RemoteMotion oldRemote = fixture.Remote;
|
||||
AcDream.Runtime.Physics.RemoteMotion oldRemote = fixture.Remote;
|
||||
ulong oldAuthority = oldRecord.PositionAuthorityVersion;
|
||||
int publications = 0;
|
||||
|
||||
|
|
@ -628,9 +648,9 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
using var fixture = new BoundaryRemoteFixture();
|
||||
LiveEntityRecord oldRecord = fixture.Record;
|
||||
WorldEntity oldEntity = fixture.Entity;
|
||||
AcDream.App.Physics.RemoteMotion oldRemote = fixture.Remote;
|
||||
AcDream.Runtime.Physics.RemoteMotion oldRemote = fixture.Remote;
|
||||
WorldEntity? replacementEntity = null;
|
||||
AcDream.App.Physics.RemoteMotion? replacementRemote = null;
|
||||
AcDream.Runtime.Physics.RemoteMotion? replacementRemote = null;
|
||||
ulong clockEpoch = oldRecord.ObjectClockEpoch;
|
||||
|
||||
fixture.Live.ProjectionVisibilityChanged += (record, visible) =>
|
||||
|
|
@ -700,7 +720,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Assert.Equal(2, live.SpatialRemoteMotionRuntimeCount);
|
||||
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
new PhysicsEngine(),
|
||||
live.Physics,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
var published = new List<uint>();
|
||||
|
|
@ -724,7 +744,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Assert.Equal(1, live.SpatialRemoteMotionRuntimeCount);
|
||||
}
|
||||
|
||||
private static PhysicsEngine BuildBoundaryEngine()
|
||||
private static void PopulateBoundaryEngine(PhysicsEngine engine)
|
||||
{
|
||||
static TerrainSurface FlatTerrain()
|
||||
{
|
||||
|
|
@ -734,7 +754,6 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
return new TerrainSurface(heights, table);
|
||||
}
|
||||
|
||||
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
|
||||
engine.AddLandblock(
|
||||
0xA9B4FFFFu,
|
||||
FlatTerrain(),
|
||||
|
|
@ -749,9 +768,119 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Array.Empty<PortalPlane>(),
|
||||
192f,
|
||||
0f);
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static RemoteBinding BindRemote(
|
||||
WorldEntity entity,
|
||||
AcDream.Runtime.Physics.RemoteMotion remote,
|
||||
uint fullCellId = 0x01010001u)
|
||||
{
|
||||
entity.ParentCellId = fullCellId;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(new LoadedLandblock(
|
||||
(fullCellId & 0xFFFF0000u) | 0xFFFFu,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
LiveEntityRuntime live = LiveEntityRuntimeFixture.Create(
|
||||
spatial,
|
||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
|
||||
firstLocalEntityId: entity.Id);
|
||||
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
|
||||
SpawnRemote(
|
||||
entity.ServerGuid,
|
||||
fullCellId,
|
||||
entity.Position),
|
||||
localId =>
|
||||
{
|
||||
Assert.Equal(entity.Id, localId);
|
||||
return entity;
|
||||
});
|
||||
remote.CellId = fullCellId;
|
||||
live.SetRemoteMotionRuntime(entity.ServerGuid, remote);
|
||||
return new RemoteBinding(
|
||||
live,
|
||||
record,
|
||||
live.Physics.Engine,
|
||||
new RemotePhysicsUpdater(
|
||||
live.Physics,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { }));
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn SpawnRemote(
|
||||
uint guid,
|
||||
uint fullCellId,
|
||||
Vector3 position)
|
||||
{
|
||||
const PhysicsStateFlags state =
|
||||
PhysicsStateFlags.ReportCollisions;
|
||||
var serverPosition = new CreateObject.ServerPosition(
|
||||
fullCellId,
|
||||
position.X,
|
||||
position.Y,
|
||||
position.Z,
|
||||
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: serverPosition,
|
||||
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,
|
||||
serverPosition,
|
||||
0x02000001u,
|
||||
Array.Empty<CreateObject.AnimPartChange>(),
|
||||
Array.Empty<CreateObject.TextureChange>(),
|
||||
Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||
null,
|
||||
null,
|
||||
"remote physics fixture",
|
||||
null,
|
||||
null,
|
||||
0x09000001u,
|
||||
PhysicsState: (uint)state,
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
|
||||
private sealed record RemoteBinding(
|
||||
LiveEntityRuntime Live,
|
||||
LiveEntityRecord Record,
|
||||
PhysicsEngine Engine,
|
||||
RemotePhysicsUpdater Updater);
|
||||
|
||||
private static void BindHiddenRemote(LiveEntityRuntime live, uint guid)
|
||||
{
|
||||
const uint cellId = 0x01010001u;
|
||||
|
|
@ -813,7 +942,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
ParentCellId = cellId,
|
||||
})!;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
remote.Body.Position = entity.Position;
|
||||
remote.Body.Orientation = entity.Rotation;
|
||||
remote.CellId = cellId;
|
||||
|
|
@ -835,7 +964,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
|
||||
public BoundaryRemoteFixture()
|
||||
{
|
||||
Engine = BuildBoundaryEngine();
|
||||
Engine = null!;
|
||||
Spatial.AddLandblock(new LoadedLandblock(
|
||||
0xA9B4FFFFu,
|
||||
new LandBlock(),
|
||||
|
|
@ -849,6 +978,8 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Engine.ShadowObjects.Deregister(entity.Id);
|
||||
_presentation?.Forget(record);
|
||||
});
|
||||
Engine = Live.Physics.Engine;
|
||||
PopulateBoundaryEngine(Engine);
|
||||
(Record, Entity, Remote) = SpawnAndBind(
|
||||
instanceSequence: 1,
|
||||
new Vector3(191f, 10f, 50f),
|
||||
|
|
@ -862,7 +993,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
RegisterShadow(Entity, Remote);
|
||||
Assert.True(_presentation.OnLiveEntityReady(Guid));
|
||||
Updater = new RemotePhysicsUpdater(
|
||||
Engine,
|
||||
Live.Physics,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
}
|
||||
|
|
@ -873,7 +1004,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
public RemotePhysicsUpdater Updater { get; }
|
||||
public LiveEntityRecord Record { get; }
|
||||
public WorldEntity Entity { get; }
|
||||
public AcDream.App.Physics.RemoteMotion Remote { get; }
|
||||
public AcDream.Runtime.Physics.RemoteMotion Remote { get; }
|
||||
|
||||
public void HydrateDestination() =>
|
||||
Spatial.AddLandblock(new LoadedLandblock(
|
||||
|
|
@ -882,7 +1013,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Array.Empty<WorldEntity>()));
|
||||
|
||||
public (LiveEntityRecord Record, WorldEntity Entity,
|
||||
AcDream.App.Physics.RemoteMotion Remote) ReplaceAtSource()
|
||||
AcDream.Runtime.Physics.RemoteMotion Remote) ReplaceAtSource()
|
||||
{
|
||||
Assert.True(Live.UnregisterLiveEntity(
|
||||
new DeleteObject.Parsed(Guid, InstanceSequence: 1),
|
||||
|
|
@ -903,7 +1034,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
}
|
||||
|
||||
private (LiveEntityRecord Record, WorldEntity Entity,
|
||||
AcDream.App.Physics.RemoteMotion Remote) SpawnAndBind(
|
||||
AcDream.Runtime.Physics.RemoteMotion Remote) SpawnAndBind(
|
||||
ushort instanceSequence,
|
||||
Vector3 position,
|
||||
bool enqueueDestination)
|
||||
|
|
@ -921,7 +1052,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
ParentCellId = SourceCell,
|
||||
});
|
||||
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
remote.Body.Position = position;
|
||||
remote.Body.Orientation = Quaternion.Identity;
|
||||
remote.CellId = SourceCell;
|
||||
|
|
@ -941,7 +1072,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
|
||||
private void RegisterShadow(
|
||||
WorldEntity entity,
|
||||
AcDream.App.Physics.RemoteMotion remote)
|
||||
AcDream.Runtime.Physics.RemoteMotion remote)
|
||||
{
|
||||
Engine.ShadowObjects.Register(
|
||||
entity.Id,
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ public sealed class RemoteTeleportControllerTests
|
|||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
})!;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
remote.Body.SnapToCell(
|
||||
sourceCell,
|
||||
entity.Position,
|
||||
|
|
@ -242,7 +242,7 @@ public sealed class RemoteTeleportControllerTests
|
|||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
})!;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position);
|
||||
live.SetRemoteMotionRuntime(guid, remote);
|
||||
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
|
||||
|
|
@ -333,7 +333,7 @@ public sealed class RemoteTeleportControllerTests
|
|||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
})!;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
remote.Body.SnapToCell(sourceCell, sourcePosition, sourcePosition);
|
||||
remote.Body.TransientState = TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable;
|
||||
|
|
@ -774,13 +774,13 @@ public sealed class RemoteTeleportControllerTests
|
|||
Assert.Throws<InvalidOperationException>(() =>
|
||||
fixture.Live.SetRemoteMotionRuntime(
|
||||
RollbackFixture.Guid,
|
||||
new AcDream.App.Physics.RemoteMotion()));
|
||||
new AcDream.Runtime.Physics.RemoteMotion()));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
fixture.Live.SetRemoteMotionRuntime(
|
||||
RollbackFixture.Guid,
|
||||
new BodyOnlyRemote(fixture.Remote.Body)));
|
||||
|
||||
var replacement = new AcDream.App.Physics.RemoteMotion(fixture.Remote.Body);
|
||||
var replacement = new AcDream.Runtime.Physics.RemoteMotion(fixture.Remote.Body);
|
||||
fixture.Live.SetRemoteMotionRuntime(RollbackFixture.Guid, replacement);
|
||||
|
||||
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock));
|
||||
|
|
@ -811,7 +811,7 @@ public sealed class RemoteTeleportControllerTests
|
|||
Assert.Throws<InvalidOperationException>(() =>
|
||||
fixture.Live.SetRemoteMotionRuntime(
|
||||
RollbackFixture.Guid,
|
||||
new AcDream.App.Physics.RemoteMotion()));
|
||||
new AcDream.Runtime.Physics.RemoteMotion()));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
fixture.Live.SetRemoteMotionRuntime(
|
||||
RollbackFixture.Guid,
|
||||
|
|
@ -1115,7 +1115,7 @@ public sealed class RemoteTeleportControllerTests
|
|||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
})!;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position);
|
||||
live.SetRemoteMotionRuntime(guid, remote);
|
||||
Assert.True(live.TryGetRecord(guid, out owner));
|
||||
|
|
@ -1193,7 +1193,7 @@ public sealed class RemoteTeleportControllerTests
|
|||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
})!;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position);
|
||||
live.SetRemoteMotionRuntime(guid, remote);
|
||||
return new LoadedRemoteFixture(
|
||||
|
|
@ -1238,7 +1238,7 @@ public sealed class RemoteTeleportControllerTests
|
|||
private readonly record struct LoadedRemoteFixture(
|
||||
LiveEntityRuntime Live,
|
||||
WorldEntity Entity,
|
||||
AcDream.App.Physics.RemoteMotion Remote,
|
||||
AcDream.Runtime.Physics.RemoteMotion Remote,
|
||||
PhysicsEngine Engine);
|
||||
|
||||
private static void AddDestination(PhysicsEngine engine) =>
|
||||
|
|
@ -1397,7 +1397,7 @@ public sealed class RemoteTeleportControllerTests
|
|||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
})!;
|
||||
Remote = new AcDream.App.Physics.RemoteMotion();
|
||||
Remote = new AcDream.Runtime.Physics.RemoteMotion();
|
||||
Remote.Body.SnapToCell(
|
||||
celllessSource ? 0u : SourceCell,
|
||||
SourcePosition,
|
||||
|
|
@ -1479,7 +1479,7 @@ public sealed class RemoteTeleportControllerTests
|
|||
internal GpuWorldState Spatial { get; } = new();
|
||||
internal LiveEntityRuntime Live { get; }
|
||||
internal WorldEntity Entity { get; }
|
||||
internal AcDream.App.Physics.RemoteMotion Remote { get; }
|
||||
internal AcDream.Runtime.Physics.RemoteMotion Remote { get; }
|
||||
internal PhysicsEngine Engine { get; } = new() { DataCache = new PhysicsDataCache() };
|
||||
internal RemoteTeleportController Controller { get; }
|
||||
internal LiveEntityPresentationController Presentation { get; }
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public sealed class RemoteTeleportPlacementTests
|
|||
0x01010001u,
|
||||
new Vector3(3f, 4f, 5f),
|
||||
new Vector3(3f, 4f, 5f));
|
||||
var remote = new AcDream.App.Physics.RemoteMotion(body);
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion(body);
|
||||
Quaternion orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f);
|
||||
|
||||
RemoteTeleportPlacement.Apply(
|
||||
|
|
@ -54,7 +54,7 @@ public sealed class RemoteTeleportPlacementTests
|
|||
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
|
||||
};
|
||||
var contactPlane = new Plane(Vector3.UnitZ, 0f);
|
||||
var remote = new AcDream.App.Physics.RemoteMotion(body)
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion(body)
|
||||
{
|
||||
Airborne = true,
|
||||
};
|
||||
|
|
@ -97,7 +97,7 @@ public sealed class RemoteTeleportPlacementTests
|
|||
// landblock is absent. SetPositionInternal must still receive the
|
||||
// grounded source edge captured before parking.
|
||||
body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
|
||||
var remote = new AcDream.App.Physics.RemoteMotion(body)
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion(body)
|
||||
{
|
||||
Airborne = true,
|
||||
};
|
||||
|
|
@ -141,7 +141,7 @@ public sealed class RemoteTeleportPlacementTests
|
|||
// calc_acceleration, which clears grounded angular drift, before
|
||||
// set_on_walkable installs the steep destination's false value.
|
||||
body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
|
||||
var remote = new AcDream.App.Physics.RemoteMotion(body);
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion(body);
|
||||
|
||||
RemoteTeleportPlacement.Apply(
|
||||
remote,
|
||||
|
|
@ -171,7 +171,7 @@ public sealed class RemoteTeleportPlacementTests
|
|||
{
|
||||
var original = new Vector3(7f, 8f, 9f);
|
||||
var body = new PhysicsBody { Position = original };
|
||||
var remote = new AcDream.App.Physics.RemoteMotion(body);
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion(body);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
RemoteTeleportPlacement.Apply(
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
public void HiddenRemotePose_IsSampledOnceAfterAdmittedHiddenQuantum()
|
||||
{
|
||||
var (live, record, animation) = BuildHiddenAnimated(RemoteGuid);
|
||||
var remote = new AcDream.App.Physics.RemoteMotion
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion
|
||||
{
|
||||
CellId = Cell,
|
||||
};
|
||||
|
|
@ -173,7 +173,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
var (live, record, entity) = BuildMaterialized(
|
||||
RemoteGuid,
|
||||
PhysicsStateFlags.Gravity);
|
||||
var remote = new AcDream.App.Physics.RemoteMotion
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion
|
||||
{
|
||||
CellId = Cell,
|
||||
Airborne = true,
|
||||
|
|
@ -290,7 +290,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
var physics = new PhysicsEngine();
|
||||
var projectiles = new ProjectileController(live, physics);
|
||||
record.FinalPhysicsState = ProjectileState;
|
||||
var remote = new AcDream.App.Physics.RemoteMotion
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion
|
||||
{
|
||||
CellId = Cell,
|
||||
Airborne = false,
|
||||
|
|
@ -602,9 +602,9 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
Assert.Equal(1, rootPublishes);
|
||||
}
|
||||
|
||||
private static AcDream.App.Physics.RemoteMotion BuildRemote(WorldEntity entity)
|
||||
private static AcDream.Runtime.Physics.RemoteMotion BuildRemote(WorldEntity entity)
|
||||
{
|
||||
var remote = new AcDream.App.Physics.RemoteMotion
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion
|
||||
{
|
||||
CellId = Cell,
|
||||
};
|
||||
|
|
@ -625,13 +625,13 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
PhysicsEngine? physics = null,
|
||||
ProjectileController? projectiles = null)
|
||||
{
|
||||
physics ??= new PhysicsEngine();
|
||||
physics ??= live.Physics.Engine;
|
||||
var remotePhysics = new RemotePhysicsUpdater(
|
||||
physics,
|
||||
live.Physics,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
var ordinaryPhysics = new LiveEntityOrdinaryPhysicsUpdater(
|
||||
physics,
|
||||
live.Physics,
|
||||
(_, _) => (0.48f, 1.835f));
|
||||
var identity = new LocalPlayerIdentityState { ServerGuid = localPlayerGuid };
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
|
|
|
|||
148
tests/AcDream.App.Tests/Runtime/RuntimePhysicsOwnershipTests.cs
Normal file
148
tests/AcDream.App.Tests/Runtime/RuntimePhysicsOwnershipTests.cs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AcDream.App.Tests.Runtime;
|
||||
|
||||
public sealed class RuntimePhysicsOwnershipTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProductionAppBorrowsTheRuntimePhysicsWorld()
|
||||
{
|
||||
string root = FindRepositoryRoot();
|
||||
string appRoot = Path.Combine(root, "src", "AcDream.App");
|
||||
string gameWindow = File.ReadAllText(Path.Combine(
|
||||
appRoot,
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
string app = string.Join(
|
||||
"\n",
|
||||
Directory.EnumerateFiles(appRoot, "*.cs", SearchOption.AllDirectories)
|
||||
.Select(File.ReadAllText));
|
||||
|
||||
Assert.Empty(Regex.Matches(
|
||||
app,
|
||||
@"new\s+(?:AcDream\.Core\.Physics\.)?PhysicsEngine\s*(?:\(|\{)"));
|
||||
Assert.DoesNotContain(
|
||||
"PhysicsDataCache.CreateProduction()",
|
||||
app,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_runtimeEntityObjects.Physics.Engine",
|
||||
gameWindow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_runtimeEntityObjects.Physics.DataCache",
|
||||
gameWindow,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppPhysicsFilesArePresentationAndPreparedAssetAdapters()
|
||||
{
|
||||
string root = FindRepositoryRoot();
|
||||
string appRoot = Path.Combine(root, "src", "AcDream.App");
|
||||
string runtimeRoot = Path.Combine(root, "src", "AcDream.Runtime");
|
||||
string remoteAdapter = File.ReadAllText(Path.Combine(
|
||||
appRoot,
|
||||
"Physics",
|
||||
"RemotePhysicsUpdater.cs"));
|
||||
string ordinaryAdapter = File.ReadAllText(Path.Combine(
|
||||
appRoot,
|
||||
"Physics",
|
||||
"LiveEntityOrdinaryPhysicsUpdater.cs"));
|
||||
string liveRuntime = File.ReadAllText(Path.Combine(
|
||||
appRoot,
|
||||
"World",
|
||||
"LiveEntityRuntime.cs"));
|
||||
string collisionPublisher = File.ReadAllText(Path.Combine(
|
||||
appRoot,
|
||||
"Streaming",
|
||||
"LandblockPhysicsPublisher.cs"));
|
||||
string runtimeRemote = File.ReadAllText(Path.Combine(
|
||||
runtimeRoot,
|
||||
"Physics",
|
||||
"RuntimeRemotePhysicsUpdater.cs"));
|
||||
string runtimeOrdinary = File.ReadAllText(Path.Combine(
|
||||
runtimeRoot,
|
||||
"Physics",
|
||||
"RuntimeOrdinaryPhysicsUpdater.cs"));
|
||||
string runtimePhysics = File.ReadAllText(Path.Combine(
|
||||
runtimeRoot,
|
||||
"Physics",
|
||||
"RuntimePhysicsState.cs"));
|
||||
|
||||
Assert.DoesNotContain(
|
||||
"ResolveWithTransition(",
|
||||
remoteAdapter,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"RetailObjectManagerTail.Run(",
|
||||
remoteAdapter,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"ResolveWithTransition(",
|
||||
runtimeRemote,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"RetailObjectManagerTail.Run(",
|
||||
runtimeRemote,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"ResolveWithTransition(",
|
||||
ordinaryAdapter,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"RebucketLiveEntity(",
|
||||
ordinaryAdapter,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"ResolveWithTransition(",
|
||||
runtimeOrdinary,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
Assert.Contains(
|
||||
"_physics.GetOrCreatePhysicsBody(",
|
||||
liveRuntime,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_physics.TryGetPhysicsHost(",
|
||||
liveRuntime,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"BeginCollisionAdmission(",
|
||||
collisionPublisher,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"AdmitCollisionAssets(",
|
||||
collisionPublisher,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
".AddLandblock(",
|
||||
collisionPublisher,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
".RemoveLandblock(",
|
||||
collisionPublisher,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"public PhysicsBody GetOrCreatePhysicsBody(",
|
||||
runtimePhysics,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"public bool TryGetPhysicsHost(",
|
||||
runtimePhysics,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx")))
|
||||
return current.FullName;
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("AcDream.slnx was not found.");
|
||||
}
|
||||
}
|
||||
|
|
@ -504,8 +504,11 @@ public sealed class LandblockConcretePresentationPipelineTests
|
|||
var state = new GpuWorldState(
|
||||
new LandblockSpawnAdapter(new RecordingMeshAdapter(calls)),
|
||||
entityScriptActivator: activator);
|
||||
var cache = new PhysicsDataCache();
|
||||
var engine = new PhysicsEngine { DataCache = cache };
|
||||
var entityObjects =
|
||||
new AcDream.Runtime.Entities.RuntimeEntityObjectLifetime(
|
||||
new PhysicsDataCache());
|
||||
PhysicsDataCache cache = entityObjects.Physics.DataCache;
|
||||
PhysicsEngine engine = entityObjects.Physics.Engine;
|
||||
var render = new LandblockRenderPublisher(
|
||||
(_, _, _) => calls.Add("terrain"),
|
||||
removeTerrain ?? (_ => calls.Add("terrain-remove")),
|
||||
|
|
@ -513,7 +516,9 @@ public sealed class LandblockConcretePresentationPipelineTests
|
|||
state,
|
||||
commitEnvCells: commitEnvCells,
|
||||
removeEnvCells: removeEnvCells ?? (_ => calls.Add("envcell-remove")));
|
||||
var physics = new LandblockPhysicsPublisher(engine, new float[256]);
|
||||
var physics = new LandblockPhysicsPublisher(
|
||||
entityObjects.Physics,
|
||||
new float[256]);
|
||||
var lights = new LightManager();
|
||||
var lighting = new LightingHookSink(
|
||||
lights,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Reflection;
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
|
|
@ -22,17 +23,16 @@ public sealed class LandblockPhysicsPublisherTests
|
|||
public void Constructor_ClonesHeightTableAndRejectsIncompleteInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new LandblockPhysicsPublisher(
|
||||
new PhysicsEngine(),
|
||||
new RuntimeEntityObjectLifetime().Physics,
|
||||
new float[255]));
|
||||
Assert.Throws<ArgumentException>(() => new LandblockPhysicsPublisher(
|
||||
new PhysicsEngine(),
|
||||
HeightTable));
|
||||
|
||||
float[] mutable = HeightTable.ToArray();
|
||||
var engine = new PhysicsEngine();
|
||||
var cache = new PhysicsDataCache();
|
||||
engine.DataCache = cache;
|
||||
var publisher = new LandblockPhysicsPublisher(engine, mutable);
|
||||
var lifetime = new RuntimeEntityObjectLifetime(
|
||||
new PhysicsDataCache());
|
||||
PhysicsEngine engine = lifetime.Physics.Engine;
|
||||
var publisher = new LandblockPhysicsPublisher(
|
||||
lifetime.Physics,
|
||||
mutable);
|
||||
mutable[0] = 999f;
|
||||
|
||||
LandblockPhysicsPublication receipt = Begin(
|
||||
|
|
@ -794,11 +794,12 @@ public sealed class LandblockPhysicsPublisherTests
|
|||
PhysicsEngine Engine,
|
||||
PhysicsDataCache Cache) Fixture()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
var cache = new PhysicsDataCache();
|
||||
engine.DataCache = cache;
|
||||
var lifetime = new RuntimeEntityObjectLifetime(
|
||||
new PhysicsDataCache());
|
||||
PhysicsEngine engine = lifetime.Physics.Engine;
|
||||
PhysicsDataCache cache = lifetime.Physics.DataCache;
|
||||
return (
|
||||
new LandblockPhysicsPublisher(engine, HeightTable),
|
||||
new LandblockPhysicsPublisher(lifetime.Physics, HeightTable),
|
||||
engine,
|
||||
cache);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,15 +191,20 @@ public sealed class LandblockStaticPresentationPublisherTests
|
|||
|
||||
private static FixtureState Fixture()
|
||||
{
|
||||
var cache = new PhysicsDataCache();
|
||||
var engine = new PhysicsEngine { DataCache = cache };
|
||||
var entityObjects =
|
||||
new AcDream.Runtime.Entities.RuntimeEntityObjectLifetime(
|
||||
new PhysicsDataCache());
|
||||
PhysicsDataCache cache = entityObjects.Physics.DataCache;
|
||||
PhysicsEngine engine = entityObjects.Physics.Engine;
|
||||
var gpuState = new GpuWorldState();
|
||||
var render = new LandblockRenderPublisher(
|
||||
static (_, _, _) => { },
|
||||
static _ => { },
|
||||
new CellVisibility(),
|
||||
gpuState);
|
||||
var physics = new LandblockPhysicsPublisher(engine, HeightTable);
|
||||
var physics = new LandblockPhysicsPublisher(
|
||||
entityObjects.Physics,
|
||||
HeightTable);
|
||||
var lights = new LightManager();
|
||||
var lighting = new LightingHookSink(lights, new NullPoseSource());
|
||||
var translucency = new TranslucencyFadeManager();
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
|||
watcherGuid,
|
||||
resolve: Resolve,
|
||||
position: new Vector3(9f, 0f, 0f));
|
||||
EntityPhysicsHost rebound = EntityPhysicsHost.InstallOrRebind(
|
||||
EntityPhysicsHost rebound = EntityPhysicsHostComposition.InstallOrRebind(
|
||||
runtime,
|
||||
watcherRecord,
|
||||
configured);
|
||||
|
|
@ -119,7 +119,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
|||
position: new Vector3(12f, 0f, 0f));
|
||||
Assert.Same(
|
||||
watcher,
|
||||
EntityPhysicsHost.InstallOrRebind(runtime, watcherRecord, secondConfiguration));
|
||||
EntityPhysicsHostComposition.InstallOrRebind(runtime, watcherRecord, secondConfiguration));
|
||||
Assert.Same(targetManager, watcher.TargetManager);
|
||||
Assert.Same(positionManager, watcher.PositionManager);
|
||||
Assert.Equal(12f, watcher.Position.Frame.Origin.X);
|
||||
|
|
@ -139,7 +139,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
|||
guid,
|
||||
position: new Vector3(9f, 0f, 0f));
|
||||
|
||||
EntityPhysicsHost stable = EntityPhysicsHost.SelectStableHostWithoutRebind(
|
||||
EntityPhysicsHost stable = EntityPhysicsHostComposition.SelectStableHostWithoutRebind(
|
||||
runtime,
|
||||
record,
|
||||
configuration);
|
||||
|
|
@ -151,7 +151,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
|||
Assert.Same(original, record.PhysicsHost);
|
||||
Assert.Equal(1f, original.Position.Frame.Origin.X);
|
||||
|
||||
EntityPhysicsHost committed = EntityPhysicsHost.InstallOrRebind(
|
||||
EntityPhysicsHost committed = EntityPhysicsHostComposition.InstallOrRebind(
|
||||
runtime,
|
||||
record,
|
||||
configuration);
|
||||
|
|
@ -209,7 +209,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
|||
EntityPhysicsHost configured = Host(guid, position: new Vector3(7f, 0f, 0f));
|
||||
Assert.Same(
|
||||
minimal,
|
||||
EntityPhysicsHost.InstallOrRebind(runtime, record, configured));
|
||||
EntityPhysicsHostComposition.InstallOrRebind(runtime, record, configured));
|
||||
Assert.Same(minimal, motion.Host);
|
||||
Assert.Equal(7f, motion.Host!.Position.Frame.Origin.X);
|
||||
|
||||
|
|
@ -507,7 +507,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
|||
Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f),
|
||||
MeshRefs = Array.Empty<AcDream.Core.World.MeshRef>(),
|
||||
};
|
||||
EntityPhysicsHost host = EntityPhysicsHost.CreateMinimal(record, _ => null, () => 0);
|
||||
EntityPhysicsHost host = EntityPhysicsHostComposition.CreateMinimal(record, _ => null, () => 0);
|
||||
|
||||
Assert.Equal(record.FullCellId, host.Position.ObjCellId);
|
||||
Assert.Equal(record.WorldEntity.Position, host.Position.Frame.Origin);
|
||||
|
|
@ -611,7 +611,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
|||
const uint guid = 0x7000004Bu;
|
||||
var runtime = Runtime();
|
||||
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||
EntityPhysicsHost host = EntityPhysicsHost.CreateMinimal(record, _ => null, () => 0);
|
||||
EntityPhysicsHost host = EntityPhysicsHostComposition.CreateMinimal(record, _ => null, () => 0);
|
||||
runtime.InstallPhysicsHost(record, host);
|
||||
|
||||
Assert.Empty(runtime.VisibleRecords);
|
||||
|
|
|
|||
|
|
@ -663,9 +663,9 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
playerModeSource,
|
||||
"controller.PreparePositionForCommit(",
|
||||
"_camera.EnterChaseMode(legacyCamera, retailCamera);",
|
||||
"EntityPhysicsHost.SelectStableHostWithoutRebind(",
|
||||
"EntityPhysicsHostComposition.SelectStableHostWithoutRebind(",
|
||||
"_shadow.SyncPose(",
|
||||
"EntityPhysicsHost.InstallOrRebind(",
|
||||
"EntityPhysicsHostComposition.InstallOrRebind(",
|
||||
"playerEntity.SetPosition(initial.Position);",
|
||||
"controller.CommitPreparedPosition();",
|
||||
"_controllerSlot.Controller = controller;",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using AcDream.App.Physics;
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Runtime.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ internal sealed class RemoteChaseHarness
|
|||
public const float OwnRadius = 0.3f;
|
||||
public const float StickyTargetRadius = 0.5f;
|
||||
|
||||
// Field-for-field mirror of AcDream.App.Physics.RemoteMotion construction:
|
||||
// Field-for-field mirror of AcDream.Runtime.Physics.RemoteMotion construction:
|
||||
// Contact+OnWalkable+Active, InWorld=true (the
|
||||
// R4-V5 door fix — without it the interp's detached-object guard strips
|
||||
// every dispatched transition link), and the R4-V5 #160 RemoteWeenie
|
||||
|
|
@ -98,7 +98,7 @@ internal sealed class RemoteChaseHarness
|
|||
public readonly AnimationSequencer Seq;
|
||||
public readonly MotionTableDispatchSink Sink;
|
||||
|
||||
/// <summary>R5-V5: AcDream.App.Physics.RemoteMotion.Movement twin — the ONE
|
||||
/// <summary>R5-V5: AcDream.Runtime.Physics.RemoteMotion.Movement twin — the ONE
|
||||
/// per-entity MovementManager facade owning Interp + the MoveToManager
|
||||
/// (retail CPhysicsObj::movement_manager).</summary>
|
||||
public readonly MovementManager Movement;
|
||||
|
|
@ -167,7 +167,7 @@ internal sealed class RemoteChaseHarness
|
|||
Interp.CheckForCompletedMotions = Seq.Manager.CheckForCompletedMotions;
|
||||
|
||||
// ── R5-V5: the MovementManager facade owns Interp + the moveto —
|
||||
// AcDream.App.Physics.RemoteMotion ctor + EnsureRemoteMotionBindings
|
||||
// AcDream.Runtime.Physics.RemoteMotion ctor + EnsureRemoteMotionBindings
|
||||
// factory shape verbatim (sticky binds inside the factory;
|
||||
// MakeMoveToManager after the host/Pm exist). ──
|
||||
Movement = new MovementManager(Interp)
|
||||
|
|
|
|||
415
tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs
Normal file
415
tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Physics;
|
||||
|
||||
public sealed class RuntimePhysicsStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void EntityLifetimeConstructsOneCanonicalProductionPhysicsWorld()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
|
||||
Assert.Same(
|
||||
lifetime.Physics.DataCache,
|
||||
lifetime.Physics.Engine.DataCache);
|
||||
Assert.True(lifetime.Physics.CaptureOwnership().OwnsProductionDataCache);
|
||||
Assert.Equal(0, lifetime.Physics.CaptureOwnership().LandblockCount);
|
||||
Assert.False(lifetime.Physics.CaptureOwnership().IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConcurrentRuntimeLifetimesDoNotShareMutablePhysicsState()
|
||||
{
|
||||
using var first = new RuntimeEntityObjectLifetime();
|
||||
using var second = new RuntimeEntityObjectLifetime();
|
||||
|
||||
Assert.NotSame(first.Physics, second.Physics);
|
||||
Assert.NotSame(first.Physics.Engine, second.Physics.Engine);
|
||||
Assert.NotSame(first.Physics.DataCache, second.Physics.DataCache);
|
||||
Assert.NotSame(
|
||||
first.Physics.Engine.ShadowObjects,
|
||||
second.Physics.Engine.ShadowObjects);
|
||||
Assert.NotSame(
|
||||
first.Physics.DataCache.CellGraph,
|
||||
second.Physics.DataCache.CellGraph);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhysicsOwnerDisposesWithItsEntityLifetime()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
var physics = lifetime.Physics;
|
||||
|
||||
lifetime.Dispose();
|
||||
|
||||
Assert.True(physics.CaptureOwnership().IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanonicalRecordAndPhysicsOwnerOwnRemoteComponentAndWorksets()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000001u, 1));
|
||||
var remote = new TestRemoteMotion();
|
||||
lifetime.Entities.SetPhysicsBody(record, remote.Body);
|
||||
lifetime.Entities.SetRemoteMotion(record, remote);
|
||||
|
||||
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
||||
|
||||
Assert.Same(remote, record.RemoteMotion);
|
||||
Assert.Same(remote.Body, record.PhysicsBody);
|
||||
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
||||
Assert.True(lifetime.Physics.IsSpatialRemote(record, remote));
|
||||
Assert.Equal(1, lifetime.Physics.SpatialRootCount);
|
||||
Assert.Equal(1, lifetime.Physics.SpatialRemoteCount);
|
||||
|
||||
var roots = new List<RuntimeEntityRecord>();
|
||||
var remotes = new List<RuntimeEntityRecord>();
|
||||
lifetime.Physics.CopySpatialRootsTo(roots);
|
||||
lifetime.Physics.CopySpatialRemotesTo(remotes);
|
||||
Assert.Same(record, Assert.Single(roots));
|
||||
Assert.Same(record, Assert.Single(remotes));
|
||||
|
||||
lifetime.Entities.SetRemoteMotion(record, null);
|
||||
lifetime.Physics.RefreshRemoteComponent(record);
|
||||
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
||||
Assert.Equal(0, lifetime.Physics.SpatialRemoteCount);
|
||||
|
||||
lifetime.Physics.RemoveSpatialProjection(record);
|
||||
Assert.Equal(0, lifetime.Physics.SpatialRootCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualLocalKeysInConcurrentRuntimesDoNotShareWorksets()
|
||||
{
|
||||
using var first = new RuntimeEntityObjectLifetime();
|
||||
using var second = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord firstRecord =
|
||||
first.Entities.AddActive(Spawn(0x70000002u, 1));
|
||||
RuntimeEntityRecord secondRecord =
|
||||
second.Entities.AddActive(Spawn(0x70000003u, 1));
|
||||
Assert.Equal(firstRecord.Key, secondRecord.Key);
|
||||
|
||||
first.Physics.AcknowledgeSpatialProjection(firstRecord, spatial: true);
|
||||
|
||||
Assert.True(first.Physics.IsSpatialRoot(firstRecord));
|
||||
Assert.False(second.Physics.IsSpatialRoot(secondRecord));
|
||||
Assert.Equal(1, first.Physics.SpatialRootCount);
|
||||
Assert.Equal(0, second.Physics.SpatialRootCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollisionAdmissionIsExactRuntimeScopedAndWithdrawalIsTyped()
|
||||
{
|
||||
using var first = new RuntimeEntityObjectLifetime();
|
||||
using var second = new RuntimeEntityObjectLifetime();
|
||||
RuntimeCollisionAdmission admission =
|
||||
first.Physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
||||
RuntimeCollisionAdmission newer =
|
||||
first.Physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
first.Physics.AdmitCollisionAssets(
|
||||
admission,
|
||||
CollisionAssets(0xA9B4FFFFu)));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
second.Physics.AdmitCollisionAssets(
|
||||
newer,
|
||||
CollisionAssets(0xA9B4FFFFu)));
|
||||
|
||||
first.Physics.AdmitCollisionAssets(
|
||||
newer,
|
||||
CollisionAssets(0xA9B4FFFFu));
|
||||
RuntimeCollisionAcknowledgement completed =
|
||||
first.Physics.CompleteCollisionAdmission(newer);
|
||||
|
||||
Assert.True(completed.WasResident);
|
||||
Assert.Equal(1, first.Physics.Engine.LandblockCount);
|
||||
Assert.Equal(
|
||||
0,
|
||||
first.Physics.CaptureOwnership().CollisionAdmissionCount);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
first.Physics.CompleteCollisionAdmission(newer));
|
||||
|
||||
RuntimeCollisionAcknowledgement withdrawn =
|
||||
first.Physics.WithdrawCollision(0xA9B4FFFFu);
|
||||
Assert.True(withdrawn.WasResident);
|
||||
Assert.True(withdrawn.Generation > completed.Generation);
|
||||
Assert.Equal(0, first.Physics.Engine.LandblockCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoteBindingOwnsCanonicalBodyCellAndTypedCellPublication()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000021u, 1));
|
||||
var remote = new RemoteMotion();
|
||||
var commits = new List<RuntimePhysicsCellCommit>();
|
||||
lifetime.Physics.CellCommitted += commits.Add;
|
||||
lifetime.Physics.SetRemoteMotion(record, remote);
|
||||
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
||||
|
||||
remote.CellId = 0x02020001u;
|
||||
|
||||
Assert.Same(remote, record.RemoteMotion);
|
||||
Assert.Same(remote.Body, record.PhysicsBody);
|
||||
Assert.Equal(0x02020001u, record.FullCellId);
|
||||
RuntimePhysicsCellCommit commit = Assert.Single(commits);
|
||||
Assert.Same(record, commit.Record);
|
||||
Assert.Equal(0x0101FFFFu, commit.PreviousFullCellId);
|
||||
Assert.Equal(0x02020001u, commit.FullCellId);
|
||||
Assert.True(lifetime.Physics.IsSpatialRemote(record, remote));
|
||||
|
||||
Assert.True(lifetime.Physics.ClearRemoteMotion(record));
|
||||
Assert.Null(record.RemoteMotion);
|
||||
Assert.Equal(0, lifetime.Physics.SpatialRemoteCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemotePhysicsTickUsesCanonicalRuntimeAndPublishesPoseSnapshot()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000022u, 1));
|
||||
var remote = new RemoteMotion();
|
||||
remote.Body.Position = new Vector3(10f, 20f, 5f);
|
||||
remote.Body.Orientation = Quaternion.Identity;
|
||||
remote.Body.TransientState = TransientStateFlags.Active
|
||||
| TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable;
|
||||
lifetime.Physics.SetRemoteMotion(record, remote);
|
||||
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
||||
var updater = new RuntimeRemotePhysicsUpdater(lifetime.Physics);
|
||||
RuntimeRemotePhysicsSnapshot snapshot = default;
|
||||
int publishes = 0;
|
||||
|
||||
Assert.True(updater.Tick(
|
||||
record,
|
||||
remote,
|
||||
objectScale: 1f,
|
||||
sequencer: null,
|
||||
dt: 0.1f,
|
||||
objectClockEpoch: record.ObjectClockEpoch,
|
||||
new MotionDeltaFrame
|
||||
{
|
||||
Origin = Vector3.UnitX,
|
||||
Orientation = Quaternion.Identity,
|
||||
},
|
||||
radius: 0.48f,
|
||||
height: 1.835f,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1,
|
||||
acknowledgeProjection: value =>
|
||||
{
|
||||
snapshot = value;
|
||||
publishes++;
|
||||
return true;
|
||||
}));
|
||||
|
||||
Assert.Equal(1, publishes);
|
||||
Assert.True(snapshot.Position.X > 10f);
|
||||
Assert.Equal(remote.Body.Position, snapshot.Position);
|
||||
Assert.Equal(record.FullCellId, snapshot.FullCellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhysicsBodyAcquisitionIsCanonicalAndRejectsGuidReuse()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord first =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000023u, 1));
|
||||
var body = new PhysicsBody();
|
||||
|
||||
Assert.Same(
|
||||
body,
|
||||
lifetime.Physics.GetOrCreatePhysicsBody(first, _ => body));
|
||||
Assert.Same(
|
||||
body,
|
||||
lifetime.Physics.GetOrCreatePhysicsBody(
|
||||
first,
|
||||
_ => throw new InvalidOperationException(
|
||||
"The factory must not replay.")));
|
||||
|
||||
RuntimeEntityRecord stale =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000024u, 1));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
lifetime.Physics.GetOrCreatePhysicsBody(
|
||||
stale,
|
||||
_ =>
|
||||
{
|
||||
Assert.True(lifetime.Entities.RemoveActive(stale));
|
||||
lifetime.Entities.AddActive(Spawn(0x70000024u, 2));
|
||||
return new PhysicsBody();
|
||||
}));
|
||||
Assert.Null(stale.PhysicsBody);
|
||||
Assert.False(stale.PhysicsBodyAcquisitionInProgress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhysicsHostIdentityAndLookupBelongToExactRuntimeIncarnation()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000025u, 1));
|
||||
EntityPhysicsHost initial = Host(record.ServerGuid, 1f);
|
||||
EntityPhysicsHost replacement = Host(record.ServerGuid, 2f);
|
||||
|
||||
Assert.Same(
|
||||
initial,
|
||||
lifetime.Physics.InstallOrRebindPhysicsHost(record, initial));
|
||||
Assert.True(
|
||||
lifetime.Physics.TryGetPhysicsHost(
|
||||
record.ServerGuid,
|
||||
out IPhysicsObjHost resolved));
|
||||
Assert.Same(initial, resolved);
|
||||
Assert.Same(
|
||||
initial,
|
||||
lifetime.Physics.InstallOrRebindPhysicsHost(record, replacement));
|
||||
Assert.Equal(2f, initial.Position.Frame.Origin.X);
|
||||
|
||||
Assert.True(lifetime.Entities.RemoveActive(record));
|
||||
RuntimeEntityRecord next =
|
||||
lifetime.Entities.AddActive(Spawn(record.ServerGuid, 2));
|
||||
Assert.False(
|
||||
lifetime.Physics.TryGetPhysicsHost(
|
||||
record.ServerGuid,
|
||||
out _));
|
||||
Assert.Null(next.PhysicsHost);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrdinaryCellCommitIsCanonicalBeforePublication()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000026u, 1));
|
||||
var body = new PhysicsBody();
|
||||
lifetime.Entities.SetPhysicsBody(record, body);
|
||||
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
||||
RuntimePhysicsCellCommit observed = default;
|
||||
lifetime.Physics.CellCommitted += value =>
|
||||
{
|
||||
Assert.Equal(value.FullCellId, value.Record.FullCellId);
|
||||
observed = value;
|
||||
};
|
||||
|
||||
Assert.True(lifetime.Physics.CommitOrdinaryCell(
|
||||
record,
|
||||
body,
|
||||
record.ObjectClockEpoch,
|
||||
0x02020001u,
|
||||
externalOwnerValid: null));
|
||||
|
||||
Assert.Equal(0x02020001u, record.FullCellId);
|
||||
Assert.Same(record, observed.Record);
|
||||
Assert.Equal(record.SpatialAuthorityVersion, observed.SpatialAuthorityVersion);
|
||||
}
|
||||
|
||||
private static RuntimeLandblockCollisionAssets CollisionAssets(
|
||||
uint landblockId)
|
||||
{
|
||||
var heights = new byte[81];
|
||||
var table = new float[256];
|
||||
return new RuntimeLandblockCollisionAssets(
|
||||
landblockId,
|
||||
new TerrainSurface(heights, table),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
0f,
|
||||
0f,
|
||||
0u);
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
0x0101FFFFu,
|
||||
10f,
|
||||
20f,
|
||||
5f,
|
||||
1f,
|
||||
0f,
|
||||
0f,
|
||||
0f);
|
||||
var timestamps = new PhysicsTimestamps(
|
||||
Position: 1,
|
||||
Movement: 1,
|
||||
State: 1,
|
||||
Vector: 1,
|
||||
Teleport: 0,
|
||||
ServerControlledMove: 1,
|
||||
ForcePosition: 0,
|
||||
ObjDesc: 1,
|
||||
Instance: instance);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: 0x408u,
|
||||
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,
|
||||
"fixture",
|
||||
null,
|
||||
null,
|
||||
0x09000001u,
|
||||
PhysicsState: 0x408u,
|
||||
InstanceSequence: instance,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
|
||||
private static EntityPhysicsHost Host(uint guid, float x) =>
|
||||
new(
|
||||
guid,
|
||||
getPosition: () => new Position(
|
||||
0x0101FFFFu,
|
||||
new Vector3(x, 0f, 0f),
|
||||
Quaternion.Identity),
|
||||
getVelocity: () => Vector3.Zero,
|
||||
getRadius: () => 0.48f,
|
||||
inContact: () => true,
|
||||
minterpMaxSpeed: () => null,
|
||||
curTime: () => 0d,
|
||||
physicsTimerTime: () => 0d,
|
||||
getObjectA: _ => null,
|
||||
handleUpdateTarget: _ => { },
|
||||
interruptCurrentMovement: () => { });
|
||||
|
||||
private sealed class TestRemoteMotion : IRuntimeRemoteMotion
|
||||
{
|
||||
public PhysicsBody Body { get; } = new();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue