479 lines
21 KiB
C#
479 lines
21 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Physics;
|
|
|
|
namespace AcDream.Runtime.Tests.Physics;
|
|
|
|
public sealed partial class RuntimeCollisionPrefixQuiescenceTests
|
|
{
|
|
[Fact]
|
|
public void ActivationWaitsForExactWithdrawAndPlaceReceipts()
|
|
{
|
|
using var fixture = new Fixture(bindGeneration: true);
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x70003101u,
|
|
1,
|
|
CellP,
|
|
new Vector3(11f, 40f, 0f));
|
|
RuntimeSetPositionOutcome seeded = fixture.Place(
|
|
record,
|
|
CellP,
|
|
new Vector3(11.5f, 40f, 0f));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(seeded.Projection));
|
|
|
|
RuntimePhysicsState physics = fixture.Lifetime.Physics;
|
|
RuntimeCollisionAdmission admission =
|
|
physics.BeginCollisionAdmission(PrefixP);
|
|
using PreparedLandblockCollisionGeneration prepared =
|
|
PrepareSealedMutation(physics, admission, PrefixP);
|
|
int committedNotifications = 0;
|
|
physics.CollisionGenerationCommitted += _ => committedNotifications++;
|
|
|
|
RuntimeCollisionGenerationCommit first =
|
|
physics.CommitCollisionGeneration(admission, prepared);
|
|
Assert.False(first.EngineCommitted);
|
|
Assert.False(first.Completed);
|
|
Assert.False(physics.IsSpatialRoot(record));
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawn));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Withdraw, withdrawn.Kind);
|
|
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawn.Token));
|
|
_ = SealMutation(physics, admission, prepared);
|
|
RuntimeCollisionGenerationCommit transferred =
|
|
physics.CommitCollisionGeneration(admission, prepared);
|
|
Assert.True(transferred.EngineCommitted);
|
|
Assert.False(transferred.Completed);
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot restored));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
|
|
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
|
|
RuntimeCollisionGenerationCommit completed =
|
|
physics.CommitCollisionGeneration(admission, prepared);
|
|
Assert.True(completed.EngineCommitted);
|
|
Assert.True(completed.Completed);
|
|
Assert.True(physics.IsSpatialRoot(record));
|
|
Assert.Equal(1, committedNotifications);
|
|
Assert.Equal(0, physics.CaptureOwnership().CollisionPrefixMutationCount);
|
|
Assert.Equal(0, physics.CaptureOwnership().CollisionPrefixQuiescenceCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DemotionParksOnlyIndoorResidentsAndLeavesOutdoorPresentationLive()
|
|
{
|
|
using var fixture = new Fixture(bindGeneration: true);
|
|
RuntimeEntityRecord outdoor = fixture.Add(
|
|
0x70003102u,
|
|
1,
|
|
CellP,
|
|
new Vector3(12f, 41f, 0f));
|
|
RuntimeEntityRecord indoor = fixture.Add(
|
|
0x70003103u,
|
|
1,
|
|
PrefixP | 0x0100u,
|
|
new Vector3(13f, 41f, 0f));
|
|
|
|
RuntimeCollisionMutationResult first =
|
|
fixture.Lifetime.Physics.DemoteCollisionToTerrain(PrefixP);
|
|
Assert.False(first.Completed);
|
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(outdoor));
|
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(indoor));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawal));
|
|
Assert.Equal(indoor.Key, withdrawal.Token.Entity);
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(withdrawal.Token));
|
|
|
|
RuntimeCollisionMutationResult completed =
|
|
fixture.Lifetime.Physics.DemoteCollisionToTerrain(PrefixP);
|
|
Assert.True(completed.Completed);
|
|
Assert.True(completed.Ready);
|
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(outdoor));
|
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(indoor));
|
|
Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void WithdrawalLeavesResidentUnboundUntilLaterGenerationWakesIt()
|
|
{
|
|
using var fixture = new Fixture(bindGeneration: true);
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x70003104u,
|
|
1,
|
|
CellP,
|
|
new Vector3(14f, 42f, 0f));
|
|
RuntimeSetPositionOutcome seeded = fixture.Place(
|
|
record,
|
|
CellP,
|
|
new Vector3(14.5f, 42f, 0f));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(seeded.Projection));
|
|
RuntimePhysicsState physics = fixture.Lifetime.Physics;
|
|
|
|
RuntimeCollisionMutationResult withdrawal =
|
|
physics.WithdrawCollision(PrefixP);
|
|
Assert.False(withdrawal.Completed);
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot removed));
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(removed.Token));
|
|
withdrawal = physics.WithdrawCollision(PrefixP);
|
|
Assert.True(withdrawal.Completed);
|
|
Assert.False(withdrawal.Ready);
|
|
Assert.False(physics.IsSpatialRoot(record));
|
|
|
|
RuntimeCollisionAdmission admission =
|
|
physics.BeginCollisionAdmission(PrefixP);
|
|
using PreparedLandblockCollisionGeneration prepared =
|
|
PrepareSealedMutation(physics, admission, PrefixP);
|
|
RuntimeCollisionGenerationCommit pending =
|
|
physics.CommitCollisionGeneration(admission, prepared);
|
|
Assert.False(pending.Completed);
|
|
_ = SealMutation(physics, admission, prepared);
|
|
RuntimeCollisionGenerationCommit completed =
|
|
physics.CommitCollisionGeneration(admission, prepared);
|
|
Assert.True(completed.EngineCommitted);
|
|
Assert.False(completed.Completed);
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot restored));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
|
|
completed = physics.CommitCollisionGeneration(admission, prepared);
|
|
Assert.True(completed.Completed);
|
|
Assert.True(physics.IsSpatialRoot(record));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void SessionResetClearsPreAndPostEngineMutationDebt(bool postEngine)
|
|
{
|
|
using var fixture = new Fixture(bindGeneration: true);
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x70003105u,
|
|
1,
|
|
CellP,
|
|
new Vector3(15f, 43f, 0f));
|
|
RuntimeSetPositionOutcome seeded = fixture.Place(
|
|
record,
|
|
CellP,
|
|
new Vector3(15.5f, 43f, 0f));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(seeded.Projection));
|
|
RuntimePhysicsState physics = fixture.Lifetime.Physics;
|
|
RuntimeCollisionAdmission admission =
|
|
physics.BeginCollisionAdmission(PrefixP);
|
|
using PreparedLandblockCollisionGeneration prepared =
|
|
PrepareSealedMutation(physics, admission, PrefixP);
|
|
|
|
Assert.False(physics.CommitCollisionGeneration(
|
|
admission,
|
|
prepared).Completed);
|
|
if (postEngine)
|
|
{
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawal));
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(
|
|
withdrawal.Token));
|
|
_ = SealMutation(physics, admission, prepared);
|
|
RuntimeCollisionGenerationCommit transferred =
|
|
physics.CommitCollisionGeneration(admission, prepared);
|
|
Assert.True(transferred.EngineCommitted);
|
|
Assert.False(transferred.Completed);
|
|
}
|
|
|
|
_ = fixture.Lifetime.BeginSessionClear();
|
|
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
|
|
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
|
|
Assert.Equal(0, ownership.CommittedCollisionPrefixMutationCount);
|
|
Assert.Equal(0, ownership.CollisionPrefixQuiescenceCount);
|
|
Assert.Equal(0, ownership.PendingCollisionPrefixProjectionCount);
|
|
Assert.Equal(0, ownership.CollisionAdmissionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CancelledActivationCannotCommitWhileRestoreAckIsPending()
|
|
{
|
|
using var fixture = new Fixture(bindGeneration: true);
|
|
RuntimePhysicsState physics = fixture.Lifetime.Physics;
|
|
RuntimeCollisionAdmission baselineAdmission =
|
|
physics.BeginCollisionAdmission(PrefixP);
|
|
using (PreparedLandblockCollisionGeneration baseline =
|
|
PrepareSealedMutation(physics, baselineAdmission, PrefixP))
|
|
{
|
|
Assert.False(physics.CommitCollisionGeneration(
|
|
baselineAdmission,
|
|
baseline).Completed);
|
|
_ = SealMutation(physics, baselineAdmission, baseline);
|
|
Assert.True(physics.CommitCollisionGeneration(
|
|
baselineAdmission,
|
|
baseline).Completed);
|
|
}
|
|
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x70003106u,
|
|
1,
|
|
CellP,
|
|
new Vector3(16f, 44f, 0f));
|
|
RuntimeSetPositionOutcome seeded = fixture.Place(
|
|
record,
|
|
CellP,
|
|
new Vector3(16.5f, 44f, 0f));
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(
|
|
seeded.Projection));
|
|
RuntimeCollisionAdmission admission =
|
|
physics.BeginCollisionAdmission(PrefixP);
|
|
using PreparedLandblockCollisionGeneration prepared =
|
|
PrepareSealedMutation(physics, admission, PrefixP);
|
|
|
|
RuntimeCollisionGenerationCommit parked =
|
|
physics.CommitCollisionGeneration(admission, prepared);
|
|
Assert.False(parked.EngineCommitted);
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawal));
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
|
|
|
|
Assert.False(physics.CancelCollisionGeneration(admission, prepared));
|
|
RuntimeCollisionGenerationCommit forbidden =
|
|
physics.CommitCollisionGeneration(admission, prepared);
|
|
Assert.False(forbidden.EngineCommitted);
|
|
Assert.False(forbidden.Completed);
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot restored));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
|
|
Assert.True(physics.CancelCollisionGeneration(admission, prepared));
|
|
|
|
Assert.True(physics.IsSpatialRoot(record));
|
|
Assert.True(physics.Engine.IsLandblockTerrainResident(PrefixP));
|
|
Assert.Equal(0, physics.CaptureOwnership().CollisionPrefixMutationCount);
|
|
Assert.Equal(0, physics.CaptureOwnership().CollisionPrefixQuiescenceCount);
|
|
Assert.Equal(0, physics.CaptureOwnership().CollisionAdmissionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void SupersededAdmissionCancellationRestoresExactBaselineGeneration()
|
|
{
|
|
using var fixture = new Fixture(bindGeneration: true);
|
|
RuntimePhysicsState physics = fixture.Lifetime.Physics;
|
|
RuntimeCollisionAdmission baseline =
|
|
physics.BeginCollisionAdmission(PrefixP);
|
|
using (PreparedLandblockCollisionGeneration preparedBaseline =
|
|
PrepareSealedMutation(physics, baseline, PrefixP))
|
|
{
|
|
Assert.False(physics.CommitCollisionGeneration(
|
|
baseline,
|
|
preparedBaseline).Completed);
|
|
Assert.True(physics.CommitCollisionGeneration(
|
|
baseline,
|
|
preparedBaseline).Completed);
|
|
}
|
|
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x70003107u,
|
|
1,
|
|
CellP,
|
|
new Vector3(17f, 45f, 0f));
|
|
RuntimeSetPositionOutcome seeded = fixture.Place(
|
|
record,
|
|
CellP,
|
|
new Vector3(17.5f, 45f, 0f));
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(
|
|
seeded.Projection));
|
|
|
|
RuntimeCollisionAdmission admissionA =
|
|
physics.BeginCollisionAdmission(PrefixP);
|
|
using PreparedLandblockCollisionGeneration preparedA =
|
|
PrepareSealedMutation(physics, admissionA, PrefixP);
|
|
RuntimeCollisionAdmission admissionB =
|
|
physics.BeginCollisionAdmission(PrefixP);
|
|
Assert.Equal(baseline.Generation, admissionB.PreviousGeneration);
|
|
using PreparedLandblockCollisionGeneration preparedB =
|
|
PrepareSealedMutation(physics, admissionB, PrefixP);
|
|
|
|
RuntimeCollisionGenerationCommit parked =
|
|
physics.CommitCollisionGeneration(admissionB, preparedB);
|
|
Assert.False(parked.EngineCommitted);
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawal));
|
|
|
|
Assert.False(physics.CancelCollisionGeneration(admissionB, preparedB));
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
|
|
Assert.False(physics.CancelCollisionGeneration(admissionB, preparedB));
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot restored));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
|
|
Assert.True(physics.CancelCollisionGeneration(admissionB, preparedB));
|
|
|
|
Assert.True(physics.IsSpatialRoot(record));
|
|
Assert.Equal(restored.Token.ExactCellId, record.FullCellId);
|
|
Assert.True(physics.Engine.IsLandblockTerrainResident(PrefixP));
|
|
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
|
|
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
|
|
Assert.Equal(0, ownership.CollisionPrefixQuiescenceCount);
|
|
Assert.Equal(0, ownership.CollisionAdmissionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CancellationAfterEngineCommitFinishesExactPlaceAckWithoutRollback()
|
|
{
|
|
using var fixture = new Fixture(bindGeneration: true);
|
|
RuntimePhysicsState physics = fixture.Lifetime.Physics;
|
|
RuntimeCollisionAdmission baseline =
|
|
physics.BeginCollisionAdmission(PrefixP);
|
|
using (PreparedLandblockCollisionGeneration preparedBaseline =
|
|
PrepareSealedMutation(physics, baseline, PrefixP))
|
|
{
|
|
Assert.False(physics.CommitCollisionGeneration(
|
|
baseline,
|
|
preparedBaseline).Completed);
|
|
Assert.True(physics.CommitCollisionGeneration(
|
|
baseline,
|
|
preparedBaseline).Completed);
|
|
}
|
|
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x70003108u,
|
|
1,
|
|
CellP,
|
|
new Vector3(18f, 46f, 0f));
|
|
RuntimeSetPositionOutcome seeded = fixture.Place(
|
|
record,
|
|
CellP,
|
|
new Vector3(18.5f, 46f, 0f));
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(
|
|
seeded.Projection));
|
|
RuntimeCollisionAdmission admission =
|
|
physics.BeginCollisionAdmission(PrefixP);
|
|
using PreparedLandblockCollisionGeneration prepared =
|
|
PrepareSealedMutation(physics, admission, PrefixP);
|
|
int committedNotifications = 0;
|
|
physics.CollisionGenerationCommitted += _ => committedNotifications++;
|
|
|
|
Assert.False(physics.CommitCollisionGeneration(
|
|
admission,
|
|
prepared).Completed);
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawal));
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
|
|
_ = SealMutation(physics, admission, prepared);
|
|
RuntimeCollisionGenerationCommit transferred =
|
|
physics.CommitCollisionGeneration(admission, prepared);
|
|
Assert.True(transferred.EngineCommitted);
|
|
Assert.False(transferred.Completed);
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot restored));
|
|
|
|
Assert.False(physics.CancelCollisionGeneration(admission, prepared));
|
|
Assert.True(physics.Engine.IsLandblockTerrainResident(PrefixP));
|
|
Assert.Equal(0, committedNotifications);
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
|
|
Assert.True(physics.CancelCollisionGeneration(admission, prepared));
|
|
|
|
Assert.True(physics.IsSpatialRoot(record));
|
|
Assert.True(physics.Engine.IsLandblockTerrainResident(PrefixP));
|
|
Assert.Equal(1, committedNotifications);
|
|
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
|
|
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
|
|
Assert.Equal(0, ownership.CollisionPrefixQuiescenceCount);
|
|
Assert.Equal(0, ownership.CollisionAdmissionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ColdFirstGenerationCancellationReleasesToUnavailableWithoutDebt()
|
|
{
|
|
// The injected-engine constructor is an internal fixture seam. It can
|
|
// begin with collision resident before Runtime has assigned generation
|
|
// one; production Runtime constructs an empty PhysicsEngine.
|
|
using var fixture = new Fixture(bindGeneration: true);
|
|
RuntimePhysicsState physics = fixture.Lifetime.Physics;
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x70003109u,
|
|
1,
|
|
CellP,
|
|
new Vector3(19f, 47f, 0f));
|
|
RuntimeSetPositionOutcome seeded = fixture.Place(
|
|
record,
|
|
CellP,
|
|
new Vector3(19.5f, 47f, 0f));
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(
|
|
seeded.Projection));
|
|
RuntimeCollisionAdmission admission =
|
|
physics.BeginCollisionAdmission(PrefixP);
|
|
Assert.Equal(0UL, admission.PreviousGeneration);
|
|
using PreparedLandblockCollisionGeneration prepared =
|
|
PrepareSealedMutation(physics, admission, PrefixP);
|
|
|
|
RuntimeCollisionGenerationCommit parked =
|
|
physics.CommitCollisionGeneration(admission, prepared);
|
|
Assert.False(parked.EngineCommitted);
|
|
Assert.True(physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawal));
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
|
|
Assert.True(physics.CancelCollisionGeneration(admission, prepared));
|
|
|
|
Assert.True(physics.Engine.IsLandblockTerrainResident(PrefixP));
|
|
Assert.False(physics.IsSpatialRoot(record));
|
|
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
|
|
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
|
|
Assert.Equal(0, ownership.CollisionPrefixQuiescenceCount);
|
|
Assert.Equal(0, ownership.PendingCollisionPrefixProjectionCount);
|
|
Assert.Equal(0, ownership.CollisionAdmissionCount);
|
|
RuntimeSetPositionOwnershipSnapshot placement =
|
|
physics.SetPosition.CaptureOwnership();
|
|
Assert.Equal(1, placement.UnboundDeferredCellCount);
|
|
Assert.Equal(0, placement.DeferredBucketCount);
|
|
}
|
|
|
|
private static PreparedLandblockCollisionGeneration PrepareSealedMutation(
|
|
RuntimePhysicsState physics,
|
|
RuntimeCollisionAdmission admission,
|
|
uint landblock)
|
|
{
|
|
PreparedLandblockCollisionGeneration prepared =
|
|
physics.PrepareCollisionGeneration(admission);
|
|
physics.StageCollisionAssets(
|
|
admission,
|
|
prepared,
|
|
new RuntimeLandblockCollisionAssets(
|
|
landblock,
|
|
new TerrainSurface(new byte[81], new float[256]),
|
|
Array.Empty<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
0f,
|
|
0f,
|
|
0u));
|
|
_ = SealMutation(physics, admission, prepared);
|
|
return prepared;
|
|
}
|
|
|
|
private static uint[] SealMutation(
|
|
RuntimePhysicsState physics,
|
|
RuntimeCollisionAdmission admission,
|
|
PreparedLandblockCollisionGeneration prepared)
|
|
{
|
|
while (true)
|
|
{
|
|
while (!physics.AdvanceCollisionRetainedOwnerCapture(
|
|
admission,
|
|
prepared).Completed)
|
|
{
|
|
}
|
|
foreach (uint ownerId in prepared.RetainedOwnerIds)
|
|
physics.RefreshCollisionRetainedOwner(admission, prepared, ownerId);
|
|
RuntimeCollisionSealStep seal;
|
|
do
|
|
{
|
|
seal = physics.AdvanceCollisionGenerationSeal(
|
|
admission,
|
|
prepared);
|
|
}
|
|
while (!seal.Completed && !seal.Restarted);
|
|
if (seal.Completed)
|
|
return [.. prepared.RetainedOwnerIds];
|
|
}
|
|
}
|
|
}
|