refactor(runtime): own projectile simulation
Move projectile component identity, prediction invalidation, spatial worksets, authoritative corrections, and the retail physics step into AcDream.Runtime. Keep App as the DAT-shape and presentation adapter so ACE outcomes and visible behavior remain unchanged.
This commit is contained in:
parent
fce8e7b18e
commit
2aee33569f
19 changed files with 1255 additions and 335 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
namespace AcDream.App.Tests;
|
||||
|
|
@ -20,6 +21,18 @@ internal static class LiveEntityRuntimeFixture
|
|||
return new LiveEntityRuntime(spatial, resources, lifetime);
|
||||
}
|
||||
|
||||
public static LiveEntityRuntime Create(
|
||||
GpuWorldState spatial,
|
||||
ILiveEntityResourceLifecycle resources,
|
||||
PhysicsEngine physicsEngine,
|
||||
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime(
|
||||
physicsEngine,
|
||||
firstLocalEntityId);
|
||||
return new LiveEntityRuntime(spatial, resources, lifetime);
|
||||
}
|
||||
|
||||
public static LiveEntityRuntime Create(
|
||||
GpuWorldState spatial,
|
||||
ILiveEntityResourceLifecycle resources,
|
||||
|
|
@ -47,4 +60,21 @@ internal static class LiveEntityRuntimeFixture
|
|||
runtimeComponentLifecycle,
|
||||
lifetime);
|
||||
}
|
||||
|
||||
public static LiveEntityRuntime Create(
|
||||
GpuWorldState spatial,
|
||||
ILiveEntityResourceLifecycle resources,
|
||||
ILiveEntityRuntimeComponentLifecycle runtimeComponentLifecycle,
|
||||
PhysicsEngine physicsEngine,
|
||||
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime(
|
||||
physicsEngine,
|
||||
firstLocalEntityId);
|
||||
return new LiveEntityRuntime(
|
||||
spatial,
|
||||
resources,
|
||||
runtimeComponentLifecycle,
|
||||
lifetime);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ public sealed class ProjectileControllerTests
|
|||
seedCellId: CellA,
|
||||
isStatic: false);
|
||||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 2.0));
|
||||
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
|
||||
IRuntimeProjectile runtime = record.ProjectileRuntime!;
|
||||
|
||||
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
|
||||
record,
|
||||
|
|
@ -339,6 +339,7 @@ public sealed class ProjectileControllerTests
|
|||
|
||||
Assert.Equal(0xAAB40001u, record.FullCellId);
|
||||
Assert.Equal(0xAAB40001u, record.PhysicsBody!.CellPosition.ObjCellId);
|
||||
Assert.Equal(record.PhysicsBody.Position, entity.Position);
|
||||
Assert.False(record.IsSpatiallyVisible);
|
||||
Assert.True(record.IsSpatiallyProjected);
|
||||
Assert.False(record.PhysicsBody.InWorld);
|
||||
|
|
@ -514,7 +515,7 @@ public sealed class ProjectileControllerTests
|
|||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
|
||||
Vector3 before = record.WorldEntity!.Position;
|
||||
|
||||
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
|
||||
IRuntimeProjectile runtime = record.ProjectileRuntime!;
|
||||
PhysicsBody body = runtime.Body;
|
||||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
||||
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
||||
|
|
@ -565,7 +566,7 @@ public sealed class ProjectileControllerTests
|
|||
cellLocalPosition: new Vector3(191f, 10f, 50f),
|
||||
velocity: new Vector3(40f, 0f, 0f));
|
||||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(0.5f), 0.0));
|
||||
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
|
||||
IRuntimeProjectile runtime = record.ProjectileRuntime!;
|
||||
|
||||
fixture.Controller.Tick(0.05, 0xA9, 0xB4, playerWorldPosition: null);
|
||||
Assert.Equal(destinationCell, record.FullCellId);
|
||||
|
|
@ -988,7 +989,7 @@ public sealed class ProjectileControllerTests
|
|||
Assert.Equal(MissileState, record.PhysicsBody!.State);
|
||||
Assert.True(double.IsFinite(record.PhysicsBody.LastUpdateTime));
|
||||
|
||||
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
|
||||
IRuntimeProjectile runtime = record.ProjectileRuntime!;
|
||||
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
||||
record, MissileState, 0.1, 1, 1));
|
||||
Assert.Same(runtime, record.ProjectileRuntime);
|
||||
|
|
@ -1320,14 +1321,14 @@ public sealed class ProjectileControllerTests
|
|||
LiveEntityRecord first = fixture.Spawn(instance: 7);
|
||||
Assert.True(fixture.Controller.TryBind(first, ProjectileSetup(), 1.0));
|
||||
WorldEntity firstEntity = first.WorldEntity!;
|
||||
ILiveEntityProjectileRuntime firstRuntime = first.ProjectileRuntime!;
|
||||
IRuntimeProjectile firstRuntime = first.ProjectileRuntime!;
|
||||
|
||||
Assert.True(fixture.Live.UnregisterLiveEntity(
|
||||
new DeleteObject.Parsed(Guid, InstanceSequence: 7),
|
||||
isLocalPlayer: false));
|
||||
LiveEntityRecord second = fixture.Spawn(instance: 8);
|
||||
Assert.True(fixture.Controller.TryBind(second, ProjectileSetup(), 2.0));
|
||||
ILiveEntityProjectileRuntime secondRuntime = second.ProjectileRuntime!;
|
||||
IRuntimeProjectile secondRuntime = second.ProjectileRuntime!;
|
||||
|
||||
Assert.False(fixture.Controller.TryBind(first, ProjectileSetup(), 3.0));
|
||||
|
||||
|
|
@ -1457,12 +1458,14 @@ public sealed class ProjectileControllerTests
|
|||
{
|
||||
if (loadInitialLandblock)
|
||||
Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
Live = LiveEntityRuntimeFixture.Create(Spatial, Resources);
|
||||
Engine = physicsEngine ?? new PhysicsEngine();
|
||||
Live = LiveEntityRuntimeFixture.Create(
|
||||
Spatial,
|
||||
Resources,
|
||||
Engine);
|
||||
Origin.Recenter(1, 1);
|
||||
Controller = new ProjectileController(
|
||||
Live,
|
||||
Engine,
|
||||
new SetupResolver(() => ResolvedSetup),
|
||||
new RootPosePublisher(
|
||||
publishRootPose ?? new Action<WorldEntity>(
|
||||
|
|
|
|||
|
|
@ -71,14 +71,6 @@ public sealed class RemoteTeleportControllerTests
|
|||
public void LeaveGround() { }
|
||||
}
|
||||
|
||||
private sealed class AlternatingProjectileRuntime(
|
||||
PhysicsBody first,
|
||||
PhysicsBody later) : ILiveEntityProjectileRuntime
|
||||
{
|
||||
private int _reads;
|
||||
public PhysicsBody Body => _reads++ == 0 ? first : later;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingDestination_ParksAuthoritativeFrameThenResolvesContactOnLoad()
|
||||
{
|
||||
|
|
@ -867,8 +859,10 @@ public sealed class RemoteTeleportControllerTests
|
|||
Assert.Same(canonicalBody, record.PhysicsBody);
|
||||
Assert.Same(remote, record.RemoteMotionRuntime);
|
||||
|
||||
var projectile = new AlternatingProjectileRuntime(canonicalBody, otherBody);
|
||||
fixture.Live.SetProjectileRuntime(RollbackFixture.Guid, projectile);
|
||||
IRuntimeProjectile projectile = fixture.Live.BindProjectileRuntime(
|
||||
RollbackFixture.Guid,
|
||||
canonicalBody,
|
||||
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
|
||||
|
||||
Assert.Same(canonicalBody, record.PhysicsBody);
|
||||
Assert.Same(projectile, record.ProjectileRuntime);
|
||||
|
|
|
|||
|
|
@ -234,8 +234,8 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(
|
||||
RemoteGuid,
|
||||
rootOrigin: Vector3.Zero);
|
||||
var physics = new PhysicsEngine();
|
||||
var projectiles = new ProjectileController(live, physics);
|
||||
PhysicsEngine physics = live.Physics.Engine;
|
||||
var projectiles = new ProjectileController(live);
|
||||
record.FinalPhysicsState = ProjectileState;
|
||||
Assert.True(record.ObjectClock.IsActive);
|
||||
Assert.True(projectiles.TryBind(
|
||||
|
|
@ -287,8 +287,8 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(
|
||||
RemoteGuid,
|
||||
rootOrigin: Vector3.Zero);
|
||||
var physics = new PhysicsEngine();
|
||||
var projectiles = new ProjectileController(live, physics);
|
||||
PhysicsEngine physics = live.Physics.Engine;
|
||||
var projectiles = new ProjectileController(live);
|
||||
record.FinalPhysicsState = ProjectileState;
|
||||
var remote = new AcDream.Runtime.Physics.RemoteMotion
|
||||
{
|
||||
|
|
@ -642,7 +642,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
new AnimationHookRouter(),
|
||||
poses))
|
||||
: new TestAnimationHookCaptureSink(captureHooks);
|
||||
projectiles ??= new ProjectileController(live, physics);
|
||||
projectiles ??= new ProjectileController(live);
|
||||
return new LiveEntityAnimationScheduler(
|
||||
live,
|
||||
identity,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Reflection;
|
|||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
|
|
@ -161,6 +162,18 @@ public sealed class GameWindowLiveEntityCompositionTests
|
|||
Assert.Equal(
|
||||
typeof(LiveWorldOriginState),
|
||||
Assert.Single(projectileFields, field => field.Name == "_origin").FieldType);
|
||||
Assert.Equal(
|
||||
typeof(RuntimeProjectilePhysicsUpdater),
|
||||
Assert.Single(
|
||||
projectileFields,
|
||||
field => field.Name == "_runtimeUpdater").FieldType);
|
||||
Assert.DoesNotContain(
|
||||
projectileFields,
|
||||
field => field.FieldType == typeof(PhysicsEngine)
|
||||
|| field.FieldType == typeof(ProjectilePhysicsStepper)
|
||||
|| field.FieldType.IsGenericType
|
||||
&& field.FieldType.GetGenericTypeDefinition()
|
||||
== typeof(Dictionary<,>));
|
||||
Assert.DoesNotContain(
|
||||
projectileFields,
|
||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType)
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ public sealed class LiveEntityLifecycleStressTests
|
|||
randomUnit: () => 0.5,
|
||||
canAdvanceOwner: ownerId => _effects?.CanAdvanceOwner(ownerId) ?? true);
|
||||
|
||||
EntityObjects = new RuntimeEntityObjectLifetime();
|
||||
EntityObjects = new RuntimeEntityObjectLifetime(Engine);
|
||||
Runtime = new LiveEntityRuntime(
|
||||
Spatial,
|
||||
new DelegateLiveEntityResourceLifecycle(
|
||||
|
|
@ -463,7 +463,7 @@ public sealed class LiveEntityLifecycleStressTests
|
|||
Poses);
|
||||
_effects = Effects;
|
||||
Router.Register(Effects);
|
||||
Projectiles = new ProjectileController(Runtime, Engine);
|
||||
Projectiles = new ProjectileController(Runtime);
|
||||
LiveLights = new LiveEntityLightController(
|
||||
Runtime,
|
||||
Poses,
|
||||
|
|
|
|||
|
|
@ -199,10 +199,10 @@ public sealed class LiveEntityProjectionWithdrawalControllerTests
|
|||
Live = LiveEntityRuntimeFixture.Create(
|
||||
Spatial,
|
||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
|
||||
lifecycle);
|
||||
Projectiles = new ProjectileController(
|
||||
Live,
|
||||
lifecycle,
|
||||
Physics);
|
||||
Projectiles = new ProjectileController(
|
||||
Live);
|
||||
Controller = new LiveEntityProjectionWithdrawalController(
|
||||
Live,
|
||||
Projectiles,
|
||||
|
|
|
|||
|
|
@ -53,11 +53,6 @@ public sealed class LiveEntityRuntimeTests
|
|||
public PhysicsBody Body { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class ProjectileRuntime(PhysicsBody body) : ILiveEntityProjectileRuntime
|
||||
{
|
||||
public PhysicsBody Body { get; } = body;
|
||||
}
|
||||
|
||||
private sealed class FailingRegisterResources : ILiveEntityResourceLifecycle
|
||||
{
|
||||
public int RegisterCount { get; private set; }
|
||||
|
|
@ -427,10 +422,12 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||
var animation = new AnimationRuntime(entity);
|
||||
var remote = new RemoteMotionRuntime();
|
||||
var projectile = new ProjectileRuntime(remote.Body);
|
||||
runtime.SetAnimationRuntime(guid, animation);
|
||||
runtime.SetRemoteMotionRuntime(guid, remote);
|
||||
runtime.SetProjectileRuntime(guid, projectile);
|
||||
IRuntimeProjectile projectile = runtime.BindProjectileRuntime(
|
||||
guid,
|
||||
remote.Body,
|
||||
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
|
||||
|
||||
Assert.True(runtime.IsCurrentSpatialAnimation(record, animation));
|
||||
Assert.True(runtime.IsCurrentSpatialRemoteMotion(record, remote));
|
||||
|
|
|
|||
|
|
@ -86,6 +86,201 @@ public sealed class RuntimePhysicsStateTests
|
|||
Assert.Equal(0, lifetime.Physics.SpatialRootCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanonicalRecordAndPhysicsOwnerOwnProjectileComponentAndWorkset()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000031u, 1));
|
||||
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
|
||||
lifetime.Entities.SetFinalPhysicsState(
|
||||
record,
|
||||
PhysicsStateFlags.ReportCollisions
|
||||
| PhysicsStateFlags.Missile
|
||||
| PhysicsStateFlags.AlignPath
|
||||
| PhysicsStateFlags.PathClipped);
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
Position = new Vector3(10f, 20f, 5f),
|
||||
Orientation = Quaternion.Identity,
|
||||
InWorld = true,
|
||||
TransientState = TransientStateFlags.Active,
|
||||
};
|
||||
body.SnapToCell(
|
||||
record.FullCellId,
|
||||
body.Position,
|
||||
body.Position);
|
||||
lifetime.Entities.SetPhysicsBody(record, body);
|
||||
var sphere = new ProjectileCollisionSphere(
|
||||
new Vector3(0.1f, 0f, 0f),
|
||||
0.25f);
|
||||
|
||||
IRuntimeProjectile projectile = lifetime.Physics.BindProjectile(
|
||||
record,
|
||||
body,
|
||||
sphere);
|
||||
lifetime.Physics.AcknowledgeSpatialProjection(
|
||||
record,
|
||||
spatial: true);
|
||||
|
||||
Assert.Same(projectile, record.Projectile);
|
||||
Assert.Same(body, projectile.Body);
|
||||
Assert.Equal(sphere, projectile.CollisionSphere);
|
||||
Assert.True(
|
||||
lifetime.Physics.IsSpatialProjectile(record, projectile));
|
||||
Assert.Equal(1, lifetime.Physics.SpatialProjectileCount);
|
||||
Assert.Equal(
|
||||
1,
|
||||
lifetime.Physics.CaptureOwnership().SpatialProjectileCount);
|
||||
|
||||
Assert.Same(
|
||||
projectile,
|
||||
lifetime.Physics.BindProjectile(record, body, sphere));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
lifetime.Physics.BindProjectile(
|
||||
record,
|
||||
new PhysicsBody(),
|
||||
sphere));
|
||||
|
||||
lifetime.Physics.RemoveSpatialProjection(record);
|
||||
Assert.Equal(0, lifetime.Physics.SpatialProjectileCount);
|
||||
Assert.Same(projectile, record.Projectile);
|
||||
|
||||
Assert.True(lifetime.Physics.ClearProjectile(record));
|
||||
Assert.Null(record.Projectile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectileAuthoritativeMutationInvalidatesSplitPrediction()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000032u, 1));
|
||||
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
|
||||
lifetime.Entities.SetFinalPhysicsState(
|
||||
record,
|
||||
PhysicsStateFlags.ReportCollisions
|
||||
| PhysicsStateFlags.Missile
|
||||
| PhysicsStateFlags.AlignPath
|
||||
| PhysicsStateFlags.PathClipped);
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
Position = new Vector3(10f, 20f, 5f),
|
||||
Orientation = Quaternion.Identity,
|
||||
InWorld = true,
|
||||
TransientState = TransientStateFlags.Active,
|
||||
};
|
||||
body.set_velocity(new Vector3(10f, 0f, 0f));
|
||||
body.SnapToCell(
|
||||
record.FullCellId,
|
||||
body.Position,
|
||||
body.Position);
|
||||
lifetime.Entities.SetPhysicsBody(record, body);
|
||||
IRuntimeProjectile projectile = lifetime.Physics.BindProjectile(
|
||||
record,
|
||||
body,
|
||||
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
|
||||
lifetime.Physics.AcknowledgeSpatialProjection(
|
||||
record,
|
||||
spatial: true);
|
||||
var updater =
|
||||
new RuntimeProjectilePhysicsUpdater(lifetime.Physics);
|
||||
|
||||
Assert.True(updater.TryBegin(
|
||||
record,
|
||||
quantum: 0.05f,
|
||||
record.ObjectClockEpoch,
|
||||
externalOwnerValid: null,
|
||||
out RuntimeProjectilePhysicsCommit commit));
|
||||
|
||||
lifetime.Entities.AdvanceVectorAuthority(record);
|
||||
Vector3 correction = new(7f, 8f, 9f);
|
||||
Assert.True(updater.ApplyAuthoritativeVector(
|
||||
record,
|
||||
record.VectorAuthorityVersion,
|
||||
record.VelocityAuthorityVersion,
|
||||
correction,
|
||||
Vector3.UnitZ,
|
||||
currentTime: 1.0));
|
||||
|
||||
Assert.True(
|
||||
projectile.PredictionAuthorityVersion
|
||||
> commit.PredictionAuthorityVersion);
|
||||
Assert.False(updater.Complete(
|
||||
commit,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1,
|
||||
_ => true));
|
||||
Assert.Equal(correction, body.Velocity);
|
||||
Assert.Equal(Vector3.UnitZ, body.Omega);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectileQuantumPublishesOneImmutableRuntimeFrame()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000033u, 1));
|
||||
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
|
||||
lifetime.Entities.SetFinalPhysicsState(
|
||||
record,
|
||||
PhysicsStateFlags.ReportCollisions
|
||||
| PhysicsStateFlags.Missile
|
||||
| PhysicsStateFlags.PathClipped);
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
Position = new Vector3(10f, 20f, 5f),
|
||||
Orientation = Quaternion.Identity,
|
||||
InWorld = true,
|
||||
TransientState = TransientStateFlags.Active,
|
||||
};
|
||||
body.SnapToCell(
|
||||
record.FullCellId,
|
||||
body.Position,
|
||||
body.Position);
|
||||
lifetime.Entities.SetPhysicsBody(record, body);
|
||||
lifetime.Physics.BindProjectile(
|
||||
record,
|
||||
body,
|
||||
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
|
||||
lifetime.Physics.AcknowledgeSpatialProjection(
|
||||
record,
|
||||
spatial: true);
|
||||
var updater =
|
||||
new RuntimeProjectilePhysicsUpdater(lifetime.Physics);
|
||||
|
||||
Assert.True(updater.TryBegin(
|
||||
record,
|
||||
quantum: 0.05f,
|
||||
record.ObjectClockEpoch,
|
||||
externalOwnerValid: null,
|
||||
out RuntimeProjectilePhysicsCommit commit));
|
||||
int acknowledgements = 0;
|
||||
RuntimePhysicsFrameSnapshot published = default;
|
||||
|
||||
Assert.True(updater.Complete(
|
||||
commit,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1,
|
||||
snapshot =>
|
||||
{
|
||||
published = snapshot;
|
||||
acknowledgements++;
|
||||
return true;
|
||||
}));
|
||||
|
||||
Assert.Equal(1, acknowledgements);
|
||||
Assert.Equal(body.Position, published.Position);
|
||||
Assert.Equal(body.Orientation, published.Orientation);
|
||||
Assert.Equal(record.FullCellId, published.FullCellId);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
updater.Complete(
|
||||
commit,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1,
|
||||
_ => true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualLocalKeysInConcurrentRuntimesDoNotShareWorksets()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue