diff --git a/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs b/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs
index 1144cf08..87cb5628 100644
--- a/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs
+++ b/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs
@@ -70,15 +70,46 @@ public sealed class LandblockPhysicsPublication : IDisposable
internal int RefloodCursor { get; set; }
internal bool RefloodCommitted { get; set; }
internal bool SealCommitted { get; set; }
+ internal bool EngineMutationCommitted { get; set; }
+ ///
+ /// The last Runtime mutation poll was nonterminal. This includes exact
+ /// placement acknowledgements, collision-report/shadow dispatch debt, and
+ /// Runtime's deliberate first quiescence poll.
+ ///
+ internal bool RuntimeMutationPending { get; set; }
internal bool BeginCommitted { get; set; }
internal bool CompletionCommitted { get; set; }
+ internal bool CancellationRequested { get; private set; }
public void Dispose()
{
- if (!CompletionCommitted)
- Physics.CancelCollisionGeneration(
- CollisionAdmission,
- PreparedGeneration);
+ if (!TryCancel())
+ {
+ throw new InvalidOperationException(
+ "Collision publication cancellation is waiting for exact placement acknowledgements and must remain retained.");
+ }
+ }
+
+ internal bool TryCancel()
+ {
+ if (CompletionCommitted)
+ return true;
+ CancellationRequested = true;
+ for (int poll = 0; poll < 2; poll++)
+ {
+ if (Physics.CancelCollisionGeneration(
+ CollisionAdmission,
+ PreparedGeneration))
+ {
+ return true;
+ }
+ if (Physics.CaptureOwnership()
+ .PendingCollisionPrefixProjectionCount != 0)
+ {
+ return false;
+ }
+ }
+ return false;
}
public uint LandblockId => Build.Landblock.LandblockId;
@@ -246,9 +277,14 @@ public sealed class LandblockPhysicsPublisher
publication.SetupObjectIds);
return publication;
}
- catch
+ catch (Exception publicationError)
{
- _physics.CancelCollisionGeneration(collisionAdmission, prepared);
+ if (!_physics.CancelCollisionGeneration(collisionAdmission, prepared))
+ {
+ throw new AggregateException(
+ "Collision preparation failed and its pre-engine cancellation did not converge.",
+ publicationError);
+ }
throw;
}
}
@@ -416,7 +452,7 @@ public sealed class LandblockPhysicsPublisher
/// presentation pipeline supplies the static-presentation owner callback
/// that preserves per-entity light-before-collision order.
///
- public void CompletePublication(
+ public bool CompletePublication(
LandblockPhysicsPublication publication,
Action? beforeStaticCollision = null)
{
@@ -426,7 +462,29 @@ public sealed class LandblockPhysicsPublisher
"Physics publication cannot complete before its prefix commits.");
while (!AdvanceCompleteOne(publication, beforeStaticCollision))
{
+ if (publication.RuntimeMutationPending)
+ {
+ if (!CanContinueMutationSynchronously())
+ return false;
+ // The activation transaction deliberately closes its exact
+ // quiescence boundary on one poll and transfers the engine on
+ // the next. This synchronous compatibility API may consume
+ // that finite internal suffix; the frame-budgeted pipeline
+ // always yields on the first nonterminal commit below.
+ publication.RuntimeMutationPending = false;
+ }
}
+ return true;
+ }
+
+ internal bool CanContinueMutationSynchronously()
+ {
+ RuntimePhysicsOwnershipSnapshot ownership = _physics.CaptureOwnership();
+ return ownership.PendingCollisionPrefixProjectionCount == 0
+ && ownership.PendingCollisionReportCount == 0
+ && ownership.PendingCollisionSetPositionDispatchCount == 0
+ && ownership.PendingShadowSetPositionDispatchCount == 0
+ && !ownership.IsCollisionReportDispatching;
}
///
@@ -438,11 +496,15 @@ public sealed class LandblockPhysicsPublisher
Action? beforeStaticCollision = null)
{
ValidateReceipt(publication);
+ if (publication.CancellationRequested)
+ throw new InvalidOperationException(
+ "A cancelled collision publication cannot resume.");
if (!publication.BeginCommitted)
throw new InvalidOperationException(
"Physics publication cannot complete before its prefix commits.");
if (publication.CompletionCommitted)
return true;
+ publication.RuntimeMutationPending = false;
long started = Stopwatch.GetTimestamp();
LoadedLandblock landblock = publication.Build.Landblock;
@@ -567,12 +629,20 @@ public sealed class LandblockPhysicsPublisher
_physics.CommitCollisionGeneration(
publication.CollisionAdmission,
publication.PreparedGeneration);
- if (!commit.Committed)
+ publication.EngineMutationCommitted |= commit.EngineCommitted;
+ if (!commit.Completed)
{
- // Runtime coalesces post-seal arrivals in its owner journal.
- // Resume that seal tail rather than restarting the
- // completed generation-wide capture/reflood pass.
- publication.SealCommitted = false;
+ publication.RuntimeMutationPending = true;
+ if (!commit.EngineCommitted)
+ {
+ // Runtime coalesces post-seal arrivals in its owner
+ // journal. Resume that seal tail rather than restarting
+ // the completed generation-wide capture/reflood pass.
+ publication.SealCommitted = false;
+ }
+ // Once EngineCommitted is true the replacement is canonical
+ // and cannot be resealed or rolled back. Later frames poll
+ // only the exact resident-restore acknowledgement suffix.
_completePublishTicks += Stopwatch.GetTimestamp() - started;
return false;
}
@@ -587,16 +657,54 @@ public sealed class LandblockPhysicsPublisher
return publication.CompletionCommitted;
}
- public void DemoteToTerrain(uint landblockId)
+ public bool DemoteToTerrain(uint landblockId)
{
- _physics.DemoteCollisionToTerrain(landblockId);
- _demotionCount++;
+ for (int poll = 0; poll < 2; poll++)
+ {
+ if (AdvanceDemotion(landblockId))
+ return true;
+ if (_physics.CaptureOwnership()
+ .PendingCollisionPrefixProjectionCount != 0)
+ {
+ return false;
+ }
+ }
+ return false;
}
- public void RemoveLandblock(uint landblockId)
+ internal bool AdvanceDemotion(uint landblockId)
{
- _physics.WithdrawCollision(landblockId);
+ RuntimeCollisionMutationResult result =
+ _physics.DemoteCollisionToTerrain(landblockId);
+ if (!result.Completed)
+ return false;
+ _demotionCount++;
+ return true;
+ }
+
+ public bool RemoveLandblock(uint landblockId)
+ {
+ for (int poll = 0; poll < 2; poll++)
+ {
+ if (AdvanceRemoval(landblockId))
+ return true;
+ if (_physics.CaptureOwnership()
+ .PendingCollisionPrefixProjectionCount != 0)
+ {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ internal bool AdvanceRemoval(uint landblockId)
+ {
+ RuntimeCollisionMutationResult result =
+ _physics.WithdrawCollision(landblockId);
+ if (!result.Completed)
+ return false;
_fullRemovalCount++;
+ return true;
}
private void PublishCell(
@@ -1066,7 +1174,8 @@ public sealed class LandblockPhysicsPublisher
"The physics publication receipt belongs to another publisher.",
nameof(publication));
}
- if (!publication.CompletionCommitted)
+ if (!publication.CompletionCommitted
+ && !publication.EngineMutationCommitted)
{
ObjectDisposedException.ThrowIf(
publication.PreparedGeneration.IsDisposed,
diff --git a/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs b/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs
index 5f7c9fff..75802436 100644
--- a/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs
+++ b/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs
@@ -217,14 +217,34 @@ public sealed class LandblockPresentationPipeline
///
/// Cancels retained publication receipts during a generation reset. A
- /// collision receipt owns only its private staging world until activation,
- /// so cancellation cannot withdraw or partially replace the active world.
+ /// pre-engine receipt restores its exact prior generation. A receipt that
+ /// already transferred the engine generation remains retained until its
+ /// canonical post-engine placement acknowledgement suffix completes; the
+ /// committed transfer is never rolled back.
///
- internal void CancelPendingPublications()
+ internal bool CancelPendingPublications()
{
- foreach (PublicationTransaction transaction in _publications.Values)
- transaction.PhysicsPublication?.Dispose();
- _publications.Clear();
+ if (_publications.Count == 0)
+ return true;
+ LandblockStreamResult[] pending = [.. _publications.Keys];
+ bool completed = true;
+ for (int index = 0; index < pending.Length; index++)
+ {
+ LandblockStreamResult result = pending[index];
+ if (!_publications.TryGetValue(
+ result,
+ out PublicationTransaction? transaction))
+ {
+ continue;
+ }
+ bool cancelled = transaction.PhysicsPublication is not { } physics
+ || physics.TryCancel();
+ if (cancelled)
+ _publications.Remove(result);
+ else
+ completed = false;
+ }
+ return completed && _publications.Count == 0;
}
public void ResumePublication(LandblockStreamResult result)
@@ -247,10 +267,15 @@ public sealed class LandblockPresentationPipeline
return Advance(result, transaction, meter, ensureProgress);
}
- public void AdvanceRetirements() => _retirements.Advance();
+ public void AdvanceRetirements()
+ {
+ _retirements.Advance();
+ }
- public void AdvanceRetirements(StreamingWorkMeter meter) =>
+ public void AdvanceRetirements(StreamingWorkMeter meter)
+ {
_retirements.Advance(meter);
+ }
internal void AdvancePriorityRetirement(
uint landblockId,
@@ -261,14 +286,26 @@ public sealed class LandblockPresentationPipeline
{
_retirements.BeginFull(landblockId);
if (_retirements.UsesBudgetedSteps)
+ {
_retirements.Advance();
+ if (_retirements.IsPending(landblockId)
+ && (_physicsPublisher?.CanContinueMutationSynchronously()
+ ?? true))
+ _retirements.Advance();
+ }
}
public void BeginNearLayerRetirement(uint landblockId)
{
_retirements.BeginNearLayer(landblockId);
if (_retirements.UsesBudgetedSteps)
+ {
_retirements.Advance();
+ if (_retirements.IsPending(landblockId)
+ && (_physicsPublisher?.CanContinueMutationSynchronously()
+ ?? true))
+ _retirements.Advance();
+ }
}
internal void EnqueueFullRetirement(uint landblockId) =>
@@ -754,6 +791,17 @@ public sealed class LandblockPresentationPipeline
{
return new LandblockPublicationAdvance(false, progressed);
}
+ if (transaction.PhysicsPublication.RuntimeMutationPending)
+ {
+ if (meter is not null
+ || !_physicsPublisher
+ .CanContinueMutationSynchronously())
+ {
+ return new LandblockPublicationAdvance(
+ false,
+ progressed);
+ }
+ }
}
while (!transaction.StaticPublication.CompletionCommitted)
{
diff --git a/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs b/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs
index f9e0e937..368565c7 100644
--- a/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs
+++ b/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs
@@ -73,22 +73,20 @@ public sealed class LandblockPresentationRetirementOwner
static entity => entity.ServerGuid == 0,
_staticPresentation.RemovePluginProjection);
+ if (!ticket.RunOnce(
+ LandblockRetirementStage.Physics,
+ () => ticket.Kind == LandblockRetirementKind.Full
+ ? _physics.AdvanceRemoval(ticket.LandblockId)
+ : _physics.AdvanceDemotion(ticket.LandblockId)))
+ {
+ return;
+ }
if (ticket.Kind == LandblockRetirementKind.Full)
{
ticket.RunOnce(
LandblockRetirementStage.Terrain,
() => _render.RemoveTerrain(ticket.LandblockId));
}
-
- ticket.RunOnce(
- LandblockRetirementStage.Physics,
- () =>
- {
- if (ticket.Kind == LandblockRetirementKind.Full)
- _physics.RemoveLandblock(ticket.LandblockId);
- else
- _physics.DemoteToTerrain(ticket.LandblockId);
- });
ticket.RunOnce(
LandblockRetirementStage.CellVisibility,
() => _render.RemoveCellVisibility(ticket.LandblockId));
@@ -128,13 +126,9 @@ public sealed class LandblockPresentationRetirementOwner
LandblockRetirementStage.Physics =>
ticket.RunOnceStep(
LandblockRetirementStage.Physics,
- () =>
- {
- if (ticket.Kind == LandblockRetirementKind.Full)
- _physics.RemoveLandblock(ticket.LandblockId);
- else
- _physics.DemoteToTerrain(ticket.LandblockId);
- }),
+ () => ticket.Kind == LandblockRetirementKind.Full
+ ? _physics.AdvanceRemoval(ticket.LandblockId)
+ : _physics.AdvanceDemotion(ticket.LandblockId)),
LandblockRetirementStage.CellVisibility =>
ticket.RunOnceStep(
LandblockRetirementStage.CellVisibility,
diff --git a/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs b/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs
index 82046fa1..5519758d 100644
--- a/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs
+++ b/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs
@@ -12,8 +12,8 @@ public enum LandblockRetirementStage : ushort
EntityLighting = 1 << 3,
EntityTranslucency = 1 << 4,
PluginProjection = 1 << 5,
- Terrain = 1 << 6,
- Physics = 1 << 7,
+ Physics = 1 << 6,
+ Terrain = 1 << 7,
CellVisibility = 1 << 8,
BuildingRegistry = 1 << 9,
EnvironmentCells = 1 << 10,
@@ -24,6 +24,7 @@ internal enum LandblockRetirementOperationResult : byte
{
NoWork,
Progressed,
+ Pending,
Failed,
}
@@ -77,6 +78,28 @@ public sealed class LandblockRetirementTicket
}
}
+ public bool RunOnce(LandblockRetirementStage stage, Func operation)
+ {
+ ValidateSingleStage(stage);
+ ArgumentNullException.ThrowIfNull(operation);
+ if ((CompletedStages & stage) != 0)
+ return true;
+
+ try
+ {
+ if (!operation())
+ return false;
+ CompletedStages |= stage;
+ _failures.Remove(stage);
+ return true;
+ }
+ catch (Exception error)
+ {
+ _failures[stage] = error;
+ return false;
+ }
+ }
+
internal LandblockRetirementOperationResult RunOnceStep(
LandblockRetirementStage stage,
Action operation)
@@ -100,6 +123,31 @@ public sealed class LandblockRetirementTicket
}
}
+ internal LandblockRetirementOperationResult RunOnceStep(
+ LandblockRetirementStage stage,
+ Func operation)
+ {
+ ValidateSingleStage(stage);
+ ArgumentNullException.ThrowIfNull(operation);
+ if ((CompletedStages & stage) != 0)
+ return LandblockRetirementOperationResult.NoWork;
+
+ try
+ {
+ if (!operation())
+ return LandblockRetirementOperationResult.Pending;
+
+ CompletedStages |= stage;
+ _failures.Remove(stage);
+ return LandblockRetirementOperationResult.Progressed;
+ }
+ catch (Exception error)
+ {
+ _failures[stage] = error;
+ return LandblockRetirementOperationResult.Failed;
+ }
+ }
+
public bool RunForEachEntity(
LandblockRetirementStage stage,
Func predicate,
@@ -815,6 +863,8 @@ public sealed class LandblockRetirementCoordinator
}
meter.Complete();
+ if (result == LandblockRetirementOperationResult.Pending)
+ return BudgetedAdvanceResult.Yielded;
return BudgetedAdvanceResult.Progressed;
}
diff --git a/src/AcDream.App/Streaming/StreamingController.cs b/src/AcDream.App/Streaming/StreamingController.cs
index 6f4c7912..b10e88e5 100644
--- a/src/AcDream.App/Streaming/StreamingController.cs
+++ b/src/AcDream.App/Streaming/StreamingController.cs
@@ -1362,19 +1362,23 @@ public sealed class StreamingController
{
if (!transaction.PendingPublicationsCleared)
{
+ bool cancelled = false;
if (!TryRunStreamingWork(
meter,
new StreamingWorkCost(EntityOperations: 1),
"recenter-cancel-publications",
() =>
{
- _presentation.CancelPendingPublications();
- transaction.PendingPublicationsCleared = true;
+ cancelled =
+ _presentation.CancelPendingPublications();
return true;
}))
{
return false;
}
+ transaction.PendingPublicationsCleared = cancelled;
+ if (!cancelled)
+ return false;
}
if (!TryRunStreamingWork(
meter,
@@ -1519,19 +1523,23 @@ public sealed class StreamingController
{
if (!transaction.PendingPublicationsCleared)
{
+ bool cancelled = false;
if (!TryRunStreamingWork(
meter,
new StreamingWorkCost(EntityOperations: 1),
"reload-cancel-publications",
() =>
{
- _presentation.CancelPendingPublications();
- transaction.PendingPublicationsCleared = true;
+ cancelled =
+ _presentation.CancelPendingPublications();
return true;
}))
{
return false;
}
+ transaction.PendingPublicationsCleared = cancelled;
+ if (!cancelled)
+ return false;
}
if (!TryRunStreamingWork(
meter,
diff --git a/src/AcDream.Core/AcDream.Core.csproj b/src/AcDream.Core/AcDream.Core.csproj
index 8300307e..966b25e1 100644
--- a/src/AcDream.Core/AcDream.Core.csproj
+++ b/src/AcDream.Core/AcDream.Core.csproj
@@ -26,6 +26,12 @@
<_Parameter1>AcDream.Runtime.Tests
+
+ <_Parameter1>AcDream.App.Tests
+
+
+ <_Parameter1>AcDream.Headless.Tests
+
diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs
index 7af81615..69194998 100644
--- a/src/AcDream.Core/Physics/PhysicsEngine.cs
+++ b/src/AcDream.Core/Physics/PhysicsEngine.cs
@@ -1106,7 +1106,7 @@ public sealed class PhysicsEngine
/// Register a landblock with its terrain surface, indoor cells, portal
/// planes, and world-space origin offset.
///
- public void AddLandblock(uint landblockId, TerrainSurface terrain,
+ internal void AddLandblock(uint landblockId, TerrainSurface terrain,
IReadOnlyList cells, IReadOnlyList portals,
float worldOffsetX, float worldOffsetY)
{
@@ -1120,7 +1120,7 @@ public sealed class PhysicsEngine
///
/// Remove a previously registered landblock, including its shadow objects.
///
- public void RemoveLandblock(uint landblockId)
+ internal void RemoveLandblock(uint landblockId)
{
_landblocks.Remove(landblockId);
RemoveLandblockSlot(landblockId);
@@ -1152,7 +1152,7 @@ public sealed class PhysicsEngine
/// owned by this engine. Runtime calls this only at terminal disposal;
/// ordinary streaming still uses the typed per-landblock retirement path.
///
- public void Clear()
+ internal void Clear()
{
if (_landblocks.Count != 0)
{
@@ -1173,7 +1173,7 @@ public sealed class PhysicsEngine
/// while preserving its terrain surface and world offset for Far-tier use.
/// The corresponding render-side demotion preserves the terrain slot too.
///
- public void DemoteLandblockToTerrain(uint landblockId)
+ internal void DemoteLandblockToTerrain(uint landblockId)
{
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
if (_landblocks.TryGetValue(canonical, out var landblock))
diff --git a/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs b/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs
index c903e04a..407d7a62 100644
--- a/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs
+++ b/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs
@@ -18,9 +18,37 @@ internal interface IHeadlessCollisionNeighborhood
bool IsReady(uint fullCellId);
}
-internal static class HeadlessCollisionGenerationTransaction
+internal readonly record struct HeadlessCollisionGenerationAdvance(
+ bool Completed,
+ bool Progressed,
+ bool WaitingForProjectionAcknowledgement,
+ bool YieldToCaller);
+
+internal sealed class HeadlessCollisionGenerationTransaction
{
- internal static RuntimeCollisionGenerationCommit Execute(
+ private readonly RuntimePhysicsState _physics;
+ private readonly RuntimeCollisionAdmission _admission;
+ private readonly PreparedLandblockCollisionGeneration _prepared;
+ private bool _ownerCaptureCommitted;
+ private int _refreshCursor;
+ private bool _sealCommitted;
+
+ private HeadlessCollisionGenerationTransaction(
+ RuntimePhysicsState physics,
+ RuntimeCollisionAdmission admission,
+ PreparedLandblockCollisionGeneration prepared)
+ {
+ _physics = physics;
+ _admission = admission;
+ _prepared = prepared;
+ }
+
+ internal uint LandblockId => _admission.LandblockId;
+ internal bool EngineMutationCommitted { get; private set; }
+ internal bool CompletionCommitted { get; private set; }
+ internal bool CancellationRequested { get; private set; }
+
+ internal static HeadlessCollisionGenerationTransaction Begin(
RuntimePhysicsState physics,
uint landblockId,
Action? afterAdmission,
@@ -33,60 +61,103 @@ internal static class HeadlessCollisionGenerationTransaction
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(landblockId);
PreparedLandblockCollisionGeneration? prepared = null;
- bool committed = false;
try
{
- // This hook exists so the exact post-admission/pre-prepare failure
- // boundary remains covered. Production does not install one.
afterAdmission?.Invoke(admission);
prepared = physics.PrepareCollisionGeneration(admission);
stage(admission, prepared);
-
- RuntimeCollisionOwnerCaptureStep ownerCapture;
- do
- {
- ownerCapture = physics.AdvanceCollisionRetainedOwnerCapture(
- admission,
- prepared);
- }
- while (!ownerCapture.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)
- {
- throw new InvalidOperationException(
- "Headless collision owner set changed during synchronous sealing.");
- }
-
- RuntimeCollisionGenerationCommit result =
- physics.CommitCollisionGeneration(admission, prepared);
- if (!result.Committed)
- {
- throw new InvalidOperationException(
- "Headless collision generation changed during synchronous publication.");
- }
- committed = true;
- return result;
+ return new HeadlessCollisionGenerationTransaction(
+ physics,
+ admission,
+ prepared);
}
- finally
+ catch (Exception publicationError)
{
- if (!committed)
- physics.CancelCollisionGeneration(admission, prepared);
+ if (!physics.CancelCollisionGeneration(admission, prepared))
+ {
+ throw new AggregateException(
+ "Headless collision preparation failed and its pre-engine cancellation did not converge.",
+ publicationError);
+ }
+ throw;
}
}
+
+ internal HeadlessCollisionGenerationAdvance Advance()
+ {
+ if (CancellationRequested)
+ throw new InvalidOperationException(
+ "A cancelled headless collision generation cannot resume.");
+ if (CompletionCommitted)
+ return new(true, false, false, false);
+
+ if (!EngineMutationCommitted)
+ {
+ if (!_ownerCaptureCommitted)
+ {
+ RuntimeCollisionOwnerCaptureStep capture =
+ _physics.AdvanceCollisionRetainedOwnerCapture(
+ _admission,
+ _prepared);
+ _ownerCaptureCommitted = capture.Completed;
+ return new(false, true, false, false);
+ }
+
+ if (_refreshCursor < _prepared.RetainedOwnerIds.Count)
+ {
+ _physics.RefreshCollisionRetainedOwner(
+ _admission,
+ _prepared,
+ _prepared.RetainedOwnerIds[_refreshCursor]);
+ _refreshCursor++;
+ return new(false, true, false, false);
+ }
+
+ if (!_sealCommitted)
+ {
+ RuntimeCollisionSealStep seal =
+ _physics.AdvanceCollisionGenerationSeal(
+ _admission,
+ _prepared);
+ _sealCommitted = seal.Completed;
+ if (seal.Restarted)
+ {
+ _ownerCaptureCommitted = false;
+ _refreshCursor = 0;
+ }
+ return new(false, true, false, false);
+ }
+ }
+
+ RuntimeCollisionGenerationCommit commit =
+ _physics.CommitCollisionGeneration(_admission, _prepared);
+ EngineMutationCommitted |= commit.EngineCommitted;
+ CompletionCommitted = commit.Completed;
+ bool waiting = !commit.Completed
+ && _physics.CaptureOwnership()
+ .PendingCollisionPrefixProjectionCount != 0;
+ if (!commit.Completed && !commit.EngineCommitted)
+ _sealCommitted = false;
+ return new(
+ commit.Completed,
+ Progressed: true,
+ WaitingForProjectionAcknowledgement: waiting,
+ YieldToCaller: !commit.Completed);
+ }
+
+ internal bool TryCancel()
+ {
+ if (CompletionCommitted)
+ return true;
+ CancellationRequested = true;
+ bool completed = _physics.CancelCollisionGeneration(
+ _admission,
+ _prepared);
+ if (completed && EngineMutationCommitted)
+ CompletionCommitted = true;
+ return completed;
+ }
+
}
///
@@ -97,10 +168,23 @@ internal static class HeadlessCollisionGenerationTransaction
internal sealed class HeadlessCollisionNeighborhood
: IHeadlessCollisionNeighborhood
{
+ private readonly record struct PublicationSpec(
+ uint LandblockId,
+ Vector3 Origin,
+ bool Required);
+
private readonly GameRuntime _runtime;
private readonly HeadlessProcessContentOwner
.HeadlessProcessContentLease _content;
private readonly HashSet _resident = [];
+ private readonly Queue _retirementQueue = [];
+ private readonly Queue _publicationQueue = [];
+ private HeadlessCollisionGenerationTransaction? _pendingPublication;
+ private bool _pendingPublicationCancellation;
+ private bool _resetRequired;
+ private bool _publicationPlanBuilt;
+ private uint _requestedCenterLandblock;
+ private uint _requestedFullCell;
private uint _centerLandblock;
internal HeadlessCollisionNeighborhood(
@@ -122,6 +206,21 @@ internal sealed class HeadlessCollisionNeighborhood
nameof(fullCellId),
"A collision neighborhood requires a real destination cell.");
}
+ if (_requestedCenterLandblock != center)
+ {
+ _requestedCenterLandblock = center;
+ _requestedFullCell = fullCellId;
+ _resetRequired = true;
+ _publicationPlanBuilt = false;
+ _publicationQueue.Clear();
+ _pendingPublicationCancellation =
+ _pendingPublication is not null;
+ }
+ else
+ {
+ _requestedFullCell = fullCellId;
+ }
+
if (_centerLandblock == center
&& _resident.Contains(center)
&& _runtime.EntityObjects.Physics.Engine
@@ -131,55 +230,17 @@ internal sealed class HeadlessCollisionNeighborhood
.UpdatePlayerCurrCell(fullCellId);
return;
}
-
- RetireAll();
- int centerX = (int)((center >> 24) & 0xFFu);
- int centerY = (int)((center >> 16) & 0xFFu);
- try
- {
- PublishOne(
- center,
- Vector3.Zero,
- fullCellId,
- required: true);
- for (int dx = -1; dx <= 1; dx++)
- {
- for (int dy = -1; dy <= 1; dy++)
- {
- if (dx == 0 && dy == 0)
- continue;
- int landblockX = centerX + dx;
- int landblockY = centerY + dy;
- if ((uint)landblockX > byte.MaxValue
- || (uint)landblockY > byte.MaxValue)
- {
- continue;
- }
- uint landblockId =
- ((uint)landblockX << 24)
- | ((uint)landblockY << 16)
- | 0xFFFFu;
- PublishOne(
- landblockId,
- new Vector3(dx * 192f, dy * 192f, 0f),
- fullCellId,
- required: false);
- }
- }
- _centerLandblock = center;
- _runtime.EntityObjects.Physics.Engine
- .UpdatePlayerCurrCell(fullCellId);
- }
- catch
- {
- RetireAll();
- throw;
- }
+ AdvanceWork();
}
public bool IsReady(uint fullCellId)
{
uint center = CanonicalLandblock(fullCellId);
+ if (_requestedCenterLandblock == center)
+ {
+ _requestedFullCell = fullCellId;
+ AdvanceWork();
+ }
if (_centerLandblock != center
|| !_resident.Contains(center)
|| !_runtime.EntityObjects.Physics.Engine
@@ -192,7 +253,7 @@ internal sealed class HeadlessCollisionNeighborhood
.GetCellStruct(fullCellId) is not null;
}
- private void PublishOne(
+ private HeadlessCollisionGenerationTransaction? CreatePublication(
uint landblockId,
Vector3 origin,
uint currentCellId,
@@ -207,7 +268,7 @@ internal sealed class HeadlessCollisionNeighborhood
throw new InvalidDataException(
$"Required headless landblock 0x{landblockId:X8} is missing.");
}
- return;
+ return null;
}
IReadOnlyList staticEntities =
@@ -244,7 +305,7 @@ internal sealed class HeadlessCollisionNeighborhood
landblock);
RuntimePhysicsState physics = _runtime.EntityObjects.Physics;
- _ = HeadlessCollisionGenerationTransaction.Execute(
+ return HeadlessCollisionGenerationTransaction.Begin(
physics,
landblockId,
afterAdmission: null,
@@ -294,22 +355,113 @@ internal sealed class HeadlessCollisionNeighborhood
collisions,
origin);
});
- _resident.Add(CanonicalLandblock(landblockId));
}
- private void RetireAll()
+ private void AdvanceWork()
{
- if (_resident.Count == 0)
+ if (_pendingPublicationCancellation
+ && _pendingPublication is { } cancelling)
{
- _centerLandblock = 0u;
- return;
+ bool wasCommitted = cancelling.EngineMutationCommitted;
+ if (!cancelling.TryCancel())
+ return;
+ if (wasCommitted || cancelling.EngineMutationCommitted)
+ _resident.Add(cancelling.LandblockId);
+ _pendingPublication = null;
+ _pendingPublicationCancellation = false;
}
- uint[] retiring = [.. _resident];
- _resident.Clear();
- _centerLandblock = 0u;
- foreach (uint landblock in retiring)
- _ = _runtime.EntityObjects.Physics.WithdrawCollision(landblock);
+ if (_resetRequired)
+ {
+ _retirementQueue.Clear();
+ foreach (uint landblock in _resident)
+ _retirementQueue.Enqueue(landblock);
+ _centerLandblock = 0u;
+ _resetRequired = false;
+ }
+
+ while (_retirementQueue.TryPeek(out uint retiring))
+ {
+ RuntimeCollisionMutationResult result = _runtime.EntityObjects
+ .Physics.WithdrawCollision(retiring);
+ if (!result.Completed)
+ return;
+ _retirementQueue.Dequeue();
+ _resident.Remove(retiring);
+ }
+
+ if (!_publicationPlanBuilt)
+ {
+ BuildPublicationPlan(
+ _requestedCenterLandblock,
+ _publicationQueue);
+ _publicationPlanBuilt = true;
+ }
+
+ while (true)
+ {
+ if (_pendingPublication is null)
+ {
+ if (!_publicationQueue.TryDequeue(out PublicationSpec spec))
+ {
+ _centerLandblock = _requestedCenterLandblock;
+ _runtime.EntityObjects.Physics.Engine
+ .UpdatePlayerCurrCell(_requestedFullCell);
+ return;
+ }
+ _pendingPublication = CreatePublication(
+ spec.LandblockId,
+ spec.Origin,
+ _requestedFullCell,
+ spec.Required);
+ if (_pendingPublication is null)
+ continue;
+ }
+
+ HeadlessCollisionGenerationAdvance advance =
+ _pendingPublication.Advance();
+ if (advance.Completed)
+ {
+ _resident.Add(_pendingPublication.LandblockId);
+ _pendingPublication = null;
+ continue;
+ }
+ if (advance.WaitingForProjectionAcknowledgement)
+ return;
+ if (advance.YieldToCaller)
+ return;
+ if (!advance.Progressed)
+ throw new InvalidOperationException(
+ "Headless collision publication made no progress.");
+ }
+ }
+
+ private static void BuildPublicationPlan(
+ uint center,
+ Queue destination)
+ {
+ destination.Enqueue(new PublicationSpec(
+ center,
+ Vector3.Zero,
+ Required: true));
+ int centerX = (int)((center >> 24) & 0xFFu);
+ int centerY = (int)((center >> 16) & 0xFFu);
+ for (int dx = -1; dx <= 1; dx++)
+ {
+ for (int dy = -1; dy <= 1; dy++)
+ {
+ if (dx == 0 && dy == 0)
+ continue;
+ int x = centerX + dx;
+ int y = centerY + dy;
+ if ((uint)x > byte.MaxValue || (uint)y > byte.MaxValue)
+ continue;
+ destination.Enqueue(new PublicationSpec(
+ ((uint)x << 24) | ((uint)y << 16) | 0xFFFFu,
+ new Vector3(dx * 192f, dy * 192f, 0f),
+ Required: false));
+ }
+ }
}
private static uint CanonicalLandblock(uint fullCellId) =>
diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
index 951c189f..3814f099 100644
--- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
+++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
@@ -1053,8 +1053,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
_sessionClearInProgress = true;
RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray();
Physics.CollisionReports.LeaveWorldBatch(active);
- Physics.SetPosition.ResetSession();
- Physics.CollisionReports.ResetSession();
+ Physics.ResetSessionPhysics();
Entities.BeginSessionClear();
foreach (RuntimeEntityRecord canonical in active)
{
diff --git a/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs b/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs
index b57ee1dd..8a631030 100644
--- a/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs
+++ b/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs
@@ -388,6 +388,14 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
return RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority;
}
+ // Seal the exact SetPosition owner before the irreversible no-fail
+ // suffix. Any internal invariant failure therefore leaves every
+ // canonical body/controller owner untouched.
+ _physics.SetPosition.PrepareDormantLocalActivationOwnership(
+ candidate.Record,
+ candidate.Body,
+ candidate.PreparedActivation.Token.Placement);
+
// All validation is complete. The remaining stores are callback-free,
// non-allocating, and cannot fail on this single Runtime update thread.
// The controller remains RuntimeOwnedDormant; the subsequent world
diff --git a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs
index 7b3c2e06..86ab4deb 100644
--- a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs
+++ b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs
@@ -37,6 +37,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
bool IsCollisionReportDispatching,
int CollisionPrefixQuiescenceCount,
int PendingCollisionPrefixProjectionCount,
+ int CollisionPrefixMutationCount,
+ int CommittedCollisionPrefixMutationCount,
int CollisionAdmissionCount,
int CollisionGenerationCount,
bool OwnsProductionDataCache,
@@ -75,6 +77,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
&& !IsCollisionReportDispatching
&& CollisionPrefixQuiescenceCount == 0
&& PendingCollisionPrefixProjectionCount == 0
+ && CollisionPrefixMutationCount == 0
+ && CommittedCollisionPrefixMutationCount == 0
&& CollisionAdmissionCount == 0
&& CollisionGenerationCount == 0
&& OwnsProductionDataCache;
@@ -100,11 +104,13 @@ public sealed class RuntimeCollisionAdmission
internal RuntimeCollisionAdmission(
RuntimePhysicsState owner,
uint landblockId,
- ulong generation)
+ ulong generation,
+ ulong previousGeneration)
{
Owner = owner;
LandblockId = landblockId;
Generation = generation;
+ PreviousGeneration = previousGeneration;
}
internal RuntimePhysicsState Owner { get; }
@@ -112,6 +118,32 @@ public sealed class RuntimeCollisionAdmission
internal bool Completed { get; set; }
public uint LandblockId { get; }
public ulong Generation { get; }
+ internal ulong PreviousGeneration { get; }
+}
+
+internal enum RuntimeCollisionPrefixMutationKind : byte
+{
+ Activation,
+ Demotion,
+ Withdrawal,
+}
+
+internal sealed class RuntimeCollisionPrefixMutation
+{
+ internal required RuntimeCollisionPrefixMutationKind Kind { get; init; }
+ internal required uint LandblockId { get; init; }
+ internal required ulong PreviousGeneration { get; init; }
+ internal required ulong TargetGeneration { get; init; }
+ internal ulong InvalidatedGeneration { get; init; }
+ internal required RuntimeCollisionPrefixQuiescenceToken Quiescence
+ { get; init; }
+ internal RuntimeCollisionAdmission? Admission { get; init; }
+ internal PreparedLandblockCollisionGeneration? Prepared { get; init; }
+ internal RuntimeCollisionPrefixMutationPermission Permission { get; set; }
+ internal bool EngineMutationCommitted { get; set; }
+ internal bool CancellationRequested { get; set; }
+ internal bool WasResident { get; set; }
+ internal bool Ready { get; set; }
}
public readonly record struct RuntimeCollisionAcknowledgement(
@@ -120,11 +152,23 @@ public readonly record struct RuntimeCollisionAcknowledgement(
bool WasResident,
bool Ready);
+public readonly record struct RuntimeCollisionMutationResult(
+ RuntimeCollisionAcknowledgement Acknowledgement,
+ bool Completed)
+{
+ public uint LandblockId => Acknowledgement.LandblockId;
+ public ulong Generation => Acknowledgement.Generation;
+ public bool WasResident => Acknowledgement.WasResident;
+ public bool Ready => Acknowledgement.Ready;
+}
+
public readonly record struct RuntimeCollisionGenerationCommit(
RuntimeCollisionAcknowledgement Acknowledgement,
- uint[] DirtyRetainedOwnerIds)
+ uint[] DirtyRetainedOwnerIds,
+ bool EngineCommitted,
+ bool Completed)
{
- public bool Committed => Acknowledgement.Ready;
+ public bool Committed => Completed;
}
public readonly record struct RuntimeCollisionGenerationCommitted(
@@ -1045,6 +1089,8 @@ public sealed class RuntimePhysicsState : IDisposable
_collisionAdmissions = new();
private readonly Dictionary
_preparedCollisionGenerations = new();
+ private readonly Dictionary
+ _collisionPrefixMutations = new();
private readonly CollisionOwnerMutationJournal _collisionOwnerJournal = new();
private readonly Dictionary>
_collisionOwnerSubscribers = new();
@@ -1175,6 +1221,9 @@ public sealed class RuntimePhysicsState : IDisposable
collisionReports.IsDispatching,
setPosition.CollisionPrefixQuiescenceCount,
setPosition.PendingQuiescenceProjectionCount,
+ _collisionPrefixMutations.Count,
+ _collisionPrefixMutations.Values.Count(
+ mutation => mutation.EngineMutationCommitted),
_collisionAdmissions.Count,
_collisionGenerations.Count,
ReferenceEquals(Engine.DataCache, DataCache),
@@ -1907,6 +1956,29 @@ public sealed class RuntimePhysicsState : IDisposable
_spatialRoots.Clear();
}
+ internal void ResetSessionPhysics()
+ {
+ EnsureNotDisposed();
+ // GameRuntime serializes reset after the host update loop has stopped.
+ // Reset may therefore run on the lifecycle/disposal thread rather than
+ // the retired generation's update thread. It clears every mutation
+ // owner before releasing affinity so the next generation can bind its
+ // own update thread without admitting concurrent mutation.
+ foreach ((_, PreparedLandblockCollisionGeneration prepared) in
+ _preparedCollisionGenerations)
+ {
+ prepared.Dispose();
+ }
+ _preparedCollisionGenerations.Clear();
+ _collisionPrefixMutations.Clear();
+ _collisionAdmissions.Clear();
+ SetPosition.ResetSession();
+ CollisionReports.ResetSession();
+ TrimCollisionOwnerJournal();
+ AdvanceCollisionWorldAuthority();
+ Volatile.Write(ref _collisionMutationThreadId, 0);
+ }
+
internal RuntimeCollisionPrefixQuiescenceToken
BeginCollisionPrefixQuiescence(
uint landblockId,
@@ -1957,25 +2029,29 @@ public sealed class RuntimePhysicsState : IDisposable
successorReady);
}
- internal bool CompleteCollisionPrefixQuiescence(
- in RuntimeCollisionPrefixMutationPermission permission)
- {
- EnsureNotDisposed();
- EnsureCollisionMutationThread();
- return SetPosition.CompleteCollisionPrefixQuiescence(permission);
- }
-
public RuntimeCollisionAdmission BeginCollisionAdmission(
uint landblockId)
{
EnsureNotDisposed();
EnsureCollisionMutationThread();
uint canonical = CanonicalLandblock(landblockId);
+ if (_collisionPrefixMutations.ContainsKey(canonical))
+ {
+ throw new InvalidOperationException(
+ $"Collision prefix 0x{canonical:X8} is still completing its previous mutation transaction.");
+ }
+ ulong currentGeneration = _collisionGenerations.TryGetValue(
+ canonical,
+ out ulong current)
+ ? current
+ : 0UL;
+ ulong previousGeneration = currentGeneration;
AdvanceCollisionWorldAuthority();
if (_collisionAdmissions.Remove(
canonical,
out RuntimeCollisionAdmission? superseded))
{
+ previousGeneration = superseded.PreviousGeneration;
SetPosition.CancelCollisionGeneration(
canonical,
superseded.Generation);
@@ -1986,16 +2062,13 @@ public sealed class RuntimePhysicsState : IDisposable
prepared.Dispose();
}
}
- ulong generation = _collisionGenerations.TryGetValue(
- canonical,
- out ulong current)
- ? checked(current + 1UL)
- : 1UL;
+ ulong generation = checked(currentGeneration + 1UL);
_collisionGenerations[canonical] = generation;
var admission = new RuntimeCollisionAdmission(
this,
canonical,
- generation);
+ generation,
+ previousGeneration);
_collisionAdmissions[canonical] = admission;
SetPosition.BeginCollisionGeneration(canonical, generation);
return admission;
@@ -2047,7 +2120,7 @@ public sealed class RuntimePhysicsState : IDisposable
/// collision world is never withdrawn. A stale receipt may dispose its
/// own staging storage but cannot invalidate a newer admission.
///
- internal void CancelCollisionGeneration(
+ internal bool CancelCollisionGeneration(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration? prepared = null)
{
@@ -2067,6 +2140,38 @@ public sealed class RuntimePhysicsState : IDisposable
nameof(prepared));
}
+ if (_collisionPrefixMutations.TryGetValue(
+ admission.LandblockId,
+ out RuntimeCollisionPrefixMutation? mutation))
+ {
+ if (mutation.Kind is not RuntimeCollisionPrefixMutationKind.Activation
+ || !ReferenceEquals(mutation.Admission, admission)
+ || (prepared is not null
+ && !ReferenceEquals(mutation.Prepared, prepared)))
+ {
+ return false;
+ }
+ if (mutation.EngineMutationCommitted)
+ return AdvanceCommittedActivation(mutation).Committed;
+
+ mutation.CancellationRequested = true;
+ bool previousReady = mutation.PreviousGeneration != 0UL
+ && Engine.IsLandblockTerrainResident(
+ mutation.LandblockId);
+ bool released = mutation.PreviousGeneration == 0UL
+ ? SetPosition.CancelCollisionPrefixQuiescenceToUnavailable(
+ mutation.Quiescence)
+ : CancelCollisionPrefixQuiescence(
+ mutation.Quiescence,
+ mutation.PreviousGeneration,
+ previousReady);
+ if (!released)
+ {
+ return false;
+ }
+ _collisionPrefixMutations.Remove(mutation.LandblockId);
+ }
+
prepared?.Dispose();
if (prepared is not null
&& _preparedCollisionGenerations.TryGetValue(
@@ -2090,6 +2195,7 @@ public sealed class RuntimePhysicsState : IDisposable
AdvanceCollisionWorldAuthority();
}
TrimCollisionOwnerJournal();
+ return true;
}
internal void StageCollisionAssets(
@@ -2261,8 +2367,29 @@ public sealed class RuntimePhysicsState : IDisposable
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
- ValidateAdmission(admission);
+ EnsureNotDisposed();
EnsureCollisionMutationThread();
+ ArgumentNullException.ThrowIfNull(admission);
+ ArgumentNullException.ThrowIfNull(prepared);
+
+ if (_collisionPrefixMutations.TryGetValue(
+ admission.LandblockId,
+ out RuntimeCollisionPrefixMutation? pending))
+ {
+ if (pending.Kind is not RuntimeCollisionPrefixMutationKind.Activation
+ || !ReferenceEquals(pending.Admission, admission)
+ || !ReferenceEquals(pending.Prepared, prepared))
+ {
+ throw new InvalidOperationException(
+ "A different collision-prefix mutation owns this landblock.");
+ }
+ if (pending.EngineMutationCommitted)
+ return AdvanceCommittedActivation(pending);
+ if (pending.CancellationRequested)
+ return PendingActivation(pending);
+ }
+
+ ValidateAdmission(admission);
ValidatePreparedGeneration(admission, prepared);
if (!admission.AssetsPrepared)
{
@@ -2292,8 +2419,62 @@ public sealed class RuntimePhysicsState : IDisposable
admission.Generation,
Engine.IsLandblockTerrainResident(admission.LandblockId),
Ready: false),
- Array.Empty());
+ Array.Empty(),
+ EngineCommitted: false,
+ Completed: false);
}
+
+ RuntimeCollisionPrefixMutation mutation;
+ if (!_collisionPrefixMutations.TryGetValue(
+ admission.LandblockId,
+ out mutation!))
+ {
+ RuntimeCollisionPrefixQuiescenceToken token =
+ BeginCollisionPrefixQuiescence(
+ admission.LandblockId,
+ admission.Generation,
+ includeOutdoorCells: true);
+ mutation = new RuntimeCollisionPrefixMutation
+ {
+ Kind = RuntimeCollisionPrefixMutationKind.Activation,
+ LandblockId = admission.LandblockId,
+ PreviousGeneration = admission.PreviousGeneration,
+ TargetGeneration = admission.Generation,
+ Quiescence = token,
+ Admission = admission,
+ Prepared = prepared,
+ WasResident = Engine.IsLandblockTerrainResident(
+ admission.LandblockId),
+ };
+ _collisionPrefixMutations.Add(admission.LandblockId, mutation);
+
+ // The first poll deliberately closes the prefix and marks its
+ // resident-parking pass complete, even when the prefix is empty.
+ // A later poll alone may consume mutation permission; affected
+ // residents also require their exact Withdraw acknowledgements.
+ }
+
+ if (!TryAcquireCollisionPrefixMutationPermission(
+ mutation.Quiescence,
+ out RuntimeCollisionPrefixMutationPermission permission))
+ {
+ return PendingActivation(mutation);
+ }
+ mutation.Permission = permission;
+
+ // Parking a live owner writes to the canonical shadow journal. The
+ // prepared replacement must be resealed against that exact journal
+ // tail before permission can be consumed.
+ if (!IsCollisionPrefixMutationPermissionCurrent(permission)
+ || !prepared.IsOwnerMutationReconciliationCurrent
+ || !prepared.IsReadyForActivation
+ || HasOlderPreparedGeneration(prepared)
+ || prepared.HasPendingCommittedRebase
+ || prepared.HasPendingRetirement)
+ {
+ return PendingActivation(mutation);
+ }
+
PhysicsEngine.PreparedPhysicsEngineLandblock replacement =
prepared.TakeSealedReplacement();
bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
@@ -2313,20 +2494,70 @@ public sealed class RuntimePhysicsState : IDisposable
if (later.Sequence > prepared.Sequence)
later.EnqueueCommittedRebase(replacement);
}
- admission.Completed = true;
- _collisionAdmissions.Remove(admission.LandblockId);
_preparedCollisionGenerations.Remove(admission.LandblockId);
prepared.MarkCommitted();
- TrimCollisionOwnerJournal();
- var acknowledgement = new RuntimeCollisionAcknowledgement(
- admission.LandblockId,
- admission.Generation,
- Engine.IsLandblockTerrainResident(admission.LandblockId),
- Ready: Engine.IsLandblockTerrainResident(admission.LandblockId));
+ mutation.EngineMutationCommitted = true;
+ mutation.Ready = Engine.IsLandblockTerrainResident(
+ admission.LandblockId);
+ mutation.WasResident = mutation.Ready;
SetPosition.CommitCollisionGeneration(
- acknowledgement.LandblockId,
- acknowledgement.Generation,
- acknowledgement.Ready);
+ mutation.LandblockId,
+ mutation.TargetGeneration,
+ mutation.Ready);
+ RuntimeCollisionGenerationCommit completed =
+ AdvanceCommittedActivation(mutation);
+ return completed;
+ }
+
+ private RuntimeCollisionGenerationCommit PendingActivation(
+ RuntimeCollisionPrefixMutation mutation) => new(
+ new RuntimeCollisionAcknowledgement(
+ mutation.LandblockId,
+ mutation.TargetGeneration,
+ mutation.WasResident,
+ Ready: mutation.EngineMutationCommitted && mutation.Ready),
+ Array.Empty(),
+ EngineCommitted: mutation.EngineMutationCommitted,
+ Completed: false);
+
+ private RuntimeCollisionGenerationCommit AdvanceCommittedActivation(
+ RuntimeCollisionPrefixMutation mutation)
+ {
+ if (!mutation.EngineMutationCommitted)
+ throw new InvalidOperationException(
+ "Collision activation cannot release before its engine transaction commits.");
+
+ bool completed = SetPosition.ReleaseCollisionPrefixAfterMutation(
+ mutation.Quiescence,
+ mutation.TargetGeneration,
+ mutation.Ready);
+ var acknowledgement = new RuntimeCollisionAcknowledgement(
+ mutation.LandblockId,
+ mutation.TargetGeneration,
+ mutation.WasResident,
+ mutation.Ready);
+ if (!completed)
+ {
+ return new RuntimeCollisionGenerationCommit(
+ acknowledgement,
+ Array.Empty(),
+ EngineCommitted: true,
+ Completed: false);
+ }
+
+ RuntimeCollisionAdmission admission = mutation.Admission
+ ?? throw new InvalidOperationException(
+ "Collision activation lost its admission owner.");
+ admission.Completed = true;
+ if (_collisionAdmissions.TryGetValue(
+ mutation.LandblockId,
+ out RuntimeCollisionAdmission? current)
+ && ReferenceEquals(current, admission))
+ {
+ _collisionAdmissions.Remove(mutation.LandblockId);
+ }
+ _collisionPrefixMutations.Remove(mutation.LandblockId);
+ TrimCollisionOwnerJournal();
PublishCollisionGenerationCommitted(
new RuntimeCollisionGenerationCommitted(
acknowledgement.LandblockId,
@@ -2334,67 +2565,179 @@ public sealed class RuntimePhysicsState : IDisposable
acknowledgement.Ready));
return new RuntimeCollisionGenerationCommit(
acknowledgement,
- Array.Empty());
+ Array.Empty(),
+ EngineCommitted: true,
+ Completed: true);
}
- public RuntimeCollisionAcknowledgement DemoteCollisionToTerrain(
+ private static RuntimeCollisionMutationResult PendingRetirement(
+ RuntimeCollisionPrefixMutation mutation) => new(
+ new RuntimeCollisionAcknowledgement(
+ mutation.LandblockId,
+ mutation.TargetGeneration,
+ mutation.WasResident,
+ Ready: mutation.EngineMutationCommitted && mutation.Ready),
+ Completed: false);
+
+ private void CommitCollisionInvalidation(
+ RuntimeCollisionPrefixMutation mutation)
+ {
+ _collisionGenerations[mutation.LandblockId] =
+ mutation.TargetGeneration;
+ SetPosition.CancelCollisionGeneration(
+ mutation.LandblockId,
+ mutation.InvalidatedGeneration);
+ if (_collisionAdmissions.TryGetValue(
+ mutation.LandblockId,
+ out RuntimeCollisionAdmission? currentAdmission)
+ && ReferenceEquals(currentAdmission, mutation.Admission))
+ {
+ _collisionAdmissions.Remove(mutation.LandblockId);
+ }
+ if (_preparedCollisionGenerations.TryGetValue(
+ mutation.LandblockId,
+ out PreparedLandblockCollisionGeneration? currentPrepared)
+ && ReferenceEquals(currentPrepared, mutation.Prepared))
+ {
+ _preparedCollisionGenerations.Remove(mutation.LandblockId);
+ currentPrepared.Dispose();
+ }
+ }
+
+ public RuntimeCollisionMutationResult DemoteCollisionToTerrain(
uint landblockId)
+ => AdvanceCollisionRetirementMutation(
+ landblockId,
+ RuntimeCollisionPrefixMutationKind.Demotion);
+
+ public RuntimeCollisionMutationResult WithdrawCollision(
+ uint landblockId)
+ => AdvanceCollisionRetirementMutation(
+ landblockId,
+ RuntimeCollisionPrefixMutationKind.Withdrawal);
+
+ private RuntimeCollisionMutationResult AdvanceCollisionRetirementMutation(
+ uint landblockId,
+ RuntimeCollisionPrefixMutationKind kind)
{
EnsureNotDisposed();
EnsureCollisionMutationThread();
uint canonical = CanonicalLandblock(landblockId);
- bool resident = Engine.IsLandblockTerrainResident(canonical);
- InvalidateCollisionAdmission(canonical);
- bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
- _suppressCollisionOwnerJournal = true;
- try
- {
- Engine.DemoteLandblockToTerrain(canonical);
- }
- finally
- {
- _suppressCollisionOwnerJournal = suppressOwnerJournal;
- }
- foreach ((_, PreparedLandblockCollisionGeneration prepared) in
- _preparedCollisionGenerations)
- {
- prepared.RecordDemotion(canonical);
- }
- return new RuntimeCollisionAcknowledgement(
- canonical,
- _collisionGenerations[canonical],
- resident,
- Ready: Engine.IsLandblockTerrainResident(canonical));
- }
+ if (canonical == 0u)
+ throw new ArgumentOutOfRangeException(nameof(landblockId));
+ if (kind is RuntimeCollisionPrefixMutationKind.Activation)
+ throw new ArgumentOutOfRangeException(nameof(kind));
- public RuntimeCollisionAcknowledgement WithdrawCollision(
- uint landblockId)
- {
- EnsureNotDisposed();
- EnsureCollisionMutationThread();
- uint canonical = CanonicalLandblock(landblockId);
- bool resident = Engine.IsLandblockTerrainResident(canonical);
- InvalidateCollisionAdmission(canonical);
- bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
- _suppressCollisionOwnerJournal = true;
- try
+ if (!_collisionPrefixMutations.TryGetValue(
+ canonical,
+ out RuntimeCollisionPrefixMutation? mutation))
{
- Engine.RemoveLandblock(canonical);
+ ulong currentGeneration = _collisionGenerations.TryGetValue(
+ canonical,
+ out ulong current)
+ ? current
+ : 0UL;
+ RuntimeCollisionAdmission? admission =
+ _collisionAdmissions.GetValueOrDefault(canonical);
+ ulong previousGeneration = admission?.PreviousGeneration
+ ?? currentGeneration;
+ ulong invalidatedGeneration = admission is not null
+ ? admission.Generation
+ : checked(currentGeneration + 1UL);
+ ulong targetGeneration = checked(
+ Math.Max(currentGeneration, invalidatedGeneration) + 1UL);
+ SetPosition.BeginCollisionGeneration(
+ canonical,
+ targetGeneration);
+ RuntimeCollisionPrefixQuiescenceToken token =
+ BeginCollisionPrefixQuiescence(
+ canonical,
+ targetGeneration,
+ includeOutdoorCells:
+ kind is RuntimeCollisionPrefixMutationKind.Withdrawal);
+ mutation = new RuntimeCollisionPrefixMutation
+ {
+ Kind = kind,
+ LandblockId = canonical,
+ PreviousGeneration = previousGeneration,
+ InvalidatedGeneration = invalidatedGeneration,
+ TargetGeneration = targetGeneration,
+ Quiescence = token,
+ Admission = admission,
+ Prepared = _preparedCollisionGenerations.GetValueOrDefault(
+ canonical),
+ WasResident = Engine.IsLandblockTerrainResident(canonical),
+ };
+ _collisionPrefixMutations.Add(canonical, mutation);
}
- finally
+ if (mutation.Kind != kind)
{
- _suppressCollisionOwnerJournal = suppressOwnerJournal;
+ throw new InvalidOperationException(
+ "A different collision-prefix mutation owns this landblock.");
}
- foreach ((_, PreparedLandblockCollisionGeneration prepared) in
- _preparedCollisionGenerations)
+
+ if (!mutation.EngineMutationCommitted)
{
- prepared.RecordWithdrawal(canonical);
+ if (!TryAcquireCollisionPrefixMutationPermission(
+ mutation.Quiescence,
+ out RuntimeCollisionPrefixMutationPermission permission)
+ || !IsCollisionPrefixMutationPermissionCurrent(permission))
+ {
+ return PendingRetirement(mutation);
+ }
+ mutation.Permission = permission;
+ CommitCollisionInvalidation(mutation);
+
+ bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
+ _suppressCollisionOwnerJournal = true;
+ try
+ {
+ if (kind is RuntimeCollisionPrefixMutationKind.Demotion)
+ Engine.DemoteLandblockToTerrain(canonical);
+ else
+ Engine.RemoveLandblock(canonical);
+ AdvanceCollisionWorldAuthority();
+ }
+ finally
+ {
+ _suppressCollisionOwnerJournal = suppressOwnerJournal;
+ }
+ foreach ((_, PreparedLandblockCollisionGeneration prepared) in
+ _preparedCollisionGenerations)
+ {
+ if (kind is RuntimeCollisionPrefixMutationKind.Demotion)
+ prepared.RecordDemotion(canonical);
+ else
+ prepared.RecordWithdrawal(canonical);
+ }
+ mutation.EngineMutationCommitted = true;
+ mutation.Ready = kind is RuntimeCollisionPrefixMutationKind.Demotion
+ && Engine.IsLandblockTerrainResident(canonical);
+ if (mutation.Ready)
+ {
+ SetPosition.CommitCollisionGeneration(
+ canonical,
+ mutation.TargetGeneration,
+ ready: true);
+ }
}
- return new RuntimeCollisionAcknowledgement(
- canonical,
- _collisionGenerations[canonical],
- resident,
- Ready: false);
+
+ bool completed = SetPosition.ReleaseCollisionPrefixAfterMutation(
+ mutation.Quiescence,
+ mutation.TargetGeneration,
+ mutation.Ready);
+ if (completed)
+ {
+ _collisionPrefixMutations.Remove(canonical);
+ TrimCollisionOwnerJournal();
+ }
+ return new RuntimeCollisionMutationResult(
+ new RuntimeCollisionAcknowledgement(
+ canonical,
+ mutation.TargetGeneration,
+ mutation.WasResident,
+ mutation.Ready),
+ completed);
}
public void Dispose()
@@ -2419,6 +2762,7 @@ public sealed class RuntimePhysicsState : IDisposable
_spatialRemotes.Clear();
_spatialProjectiles.Clear();
_spatialRoots.Clear();
+ _collisionPrefixMutations.Clear();
_collisionAdmissions.Clear();
_collisionGenerations.Clear();
CellCommitted = null;
@@ -2570,35 +2914,6 @@ public sealed class RuntimePhysicsState : IDisposable
}
}
- private void InvalidateCollisionAdmission(uint landblockId)
- {
- AdvanceCollisionWorldAuthority();
- ulong currentGeneration = _collisionGenerations.TryGetValue(
- landblockId,
- out ulong current)
- ? current
- : 0UL;
- ulong invalidatedGeneration = _collisionAdmissions.TryGetValue(
- landblockId,
- out RuntimeCollisionAdmission? admission)
- ? admission.Generation
- : checked(currentGeneration + 1UL);
- ulong generation = checked(
- Math.Max(currentGeneration, invalidatedGeneration) + 1UL);
- _collisionGenerations[landblockId] = generation;
- SetPosition.CancelCollisionGeneration(
- landblockId,
- invalidatedGeneration);
- _collisionAdmissions.Remove(landblockId);
- if (_preparedCollisionGenerations.Remove(
- landblockId,
- out PreparedLandblockCollisionGeneration? prepared))
- {
- prepared.Dispose();
- }
- TrimCollisionOwnerJournal();
- }
-
internal bool TryPrepareSpatialRootAdmission(RuntimeEntityRecord record)
{
EnsureNotDisposed();
@@ -2960,7 +3275,13 @@ public sealed class RuntimePhysicsState : IDisposable
{
return;
}
- subscribers.Remove(prepared);
+ for (int index = 0; index < subscribers.Count; index++)
+ {
+ if (!ReferenceEquals(subscribers[index], prepared))
+ continue;
+ subscribers.RemoveAt(index);
+ break;
+ }
if (subscribers.Count == 0)
_collisionOwnerSubscribers.Remove(ownerId);
}
diff --git a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs
index f651eb92..b817d45d 100644
--- a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs
+++ b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs
@@ -106,8 +106,7 @@ internal readonly record struct RuntimeCollisionPrefixMutationPermission(
RuntimeCollisionPrefixQuiescenceToken Quiescence,
ImmutableArray Withdrawals)
{
- internal bool IsValid => Quiescence.IsValid
- && !Withdrawals.IsDefault;
+ internal bool IsValid => Quiescence.IsValid;
}
internal readonly record struct RuntimeCollisionEvaluationAuthority(
@@ -373,8 +372,9 @@ internal sealed class RuntimeSetPositionState : IDisposable
{ get; } = [];
internal bool ResidentsParked { get; set; }
internal bool PermissionIssued { get; set; }
- internal bool AbortReleaseInProgress { get; set; }
- internal ulong AbortRestoreGeneration { get; set; }
+ internal bool ReleaseInProgress { get; set; }
+ internal ulong ReleaseGeneration { get; set; }
+ internal bool ReleaseGenerationReady { get; set; }
}
private sealed class ContactCommitGuard(
@@ -504,10 +504,10 @@ internal sealed class RuntimeSetPositionState : IDisposable
if (_collisionPrefixQuiescence.TryGetValue(
prefix,
out CollisionPrefixQuiescence? active)
- && active.AbortReleaseInProgress)
+ && active.ReleaseInProgress)
{
throw new InvalidOperationException(
- $"Collision quiescence 0x{prefix:X8}/{active.Token.OperationId} is restoring its retained generation.");
+ $"Collision quiescence 0x{prefix:X8}/{active.Token.OperationId} is releasing its retained residents.");
}
_collisionPrefixQuiescence.Remove(
prefix,
@@ -592,7 +592,9 @@ internal sealed class RuntimeSetPositionState : IDisposable
current.PermissionIssued = true;
permission = new RuntimeCollisionPrefixMutationPermission(
current.Token,
- current.RetainedWithdrawals.ToImmutableArray());
+ current.RetainedWithdrawals.Count == 0
+ ? default
+ : current.RetainedWithdrawals.ToImmutableArray());
return true;
}
@@ -635,39 +637,93 @@ internal sealed class RuntimeSetPositionState : IDisposable
return removedBeforePark;
}
- if (successorGeneration == 0UL
- || !successorReady
- || current.PendingWithdrawals.Count != 0)
+ if (successorGeneration == 0UL)
+ return false;
+
+ return AdvanceCollisionPrefixRelease(
+ token,
+ successorGeneration,
+ successorReady,
+ requireMutationPermission: false);
+ }
+
+ internal bool CancelCollisionPrefixQuiescenceToUnavailable(
+ in RuntimeCollisionPrefixQuiescenceToken token)
+ {
+ EnsureNotDisposed();
+ return AdvanceCollisionPrefixRelease(
+ token,
+ generation: 0UL,
+ ready: false,
+ requireMutationPermission: false);
+ }
+
+ internal bool ReleaseCollisionPrefixAfterMutation(
+ in RuntimeCollisionPrefixQuiescenceToken token,
+ ulong activeGeneration,
+ bool ready)
+ {
+ EnsureNotDisposed();
+ return AdvanceCollisionPrefixRelease(
+ token,
+ activeGeneration,
+ ready,
+ requireMutationPermission: true);
+ }
+
+ private bool AdvanceCollisionPrefixRelease(
+ in RuntimeCollisionPrefixQuiescenceToken token,
+ ulong generation,
+ bool ready,
+ bool requireMutationPermission)
+ {
+ if ((generation == 0UL && ready)
+ || !TryGetCurrentQuiescence(
+ token,
+ out CollisionPrefixQuiescence? state))
{
return false;
}
- if (!current.AbortReleaseInProgress)
+ CollisionPrefixQuiescence current = state!;
+ if (current.PendingWithdrawals.Count != 0)
+ return false;
+ if (requireMutationPermission
+ && !current.PermissionIssued
+ && !current.ReleaseInProgress)
{
- current.AbortReleaseInProgress = true;
- current.AbortRestoreGeneration = successorGeneration;
- current.PermissionIssued = false;
- RebindQuiescedDeferredOperations(
- token,
- successorGeneration,
- ready: true);
+ return false;
}
- else if (current.AbortRestoreGeneration != successorGeneration)
+ if (!current.ReleaseInProgress)
+ {
+ current.ReleaseInProgress = true;
+ current.ReleaseGeneration = generation;
+ current.ReleaseGenerationReady = ready;
+ current.PermissionIssued = false;
+ }
+ else if (current.ReleaseGeneration != generation
+ || current.ReleaseGenerationReady != ready)
{
return false;
}
// A re-entrant/network placement may have joined the still-closed
- // prefix after abort release started. Transfer every exact newcomer
- // on each poll before deciding the barrier can open.
- RebindQuiescedDeferredOperations(
- token,
- successorGeneration,
- ready: true);
+ // prefix after release started. Transfer every exact newcomer on each
+ // poll before deciding the barrier can open.
+ if (_operations.Count != 0)
+ {
+ RebindQuiescedDeferredOperations(
+ token,
+ ready ? generation : 0UL,
+ ready,
+ releaseUnavailable: !ready);
+ }
if (current.PendingRestorePlacements.Count != 0
|| HasQuiescedDeferredOperations(token.LandblockPrefix))
+ {
return false;
+ }
bool removed = _collisionPrefixQuiescence.Remove(
token.LandblockPrefix);
if (removed)
@@ -675,19 +731,6 @@ internal sealed class RuntimeSetPositionState : IDisposable
return removed;
}
- internal bool CompleteCollisionPrefixQuiescence(
- in RuntimeCollisionPrefixMutationPermission permission)
- {
- EnsureNotDisposed();
- if (!IsCollisionPrefixMutationPermissionCurrent(permission))
- return false;
- bool removed = _collisionPrefixQuiescence.Remove(
- permission.Quiescence.LandblockPrefix);
- if (removed)
- _physics.AdvanceCollisionQuiescenceAuthority();
- return removed;
- }
-
internal void BindEventStream(RuntimeEntityObjectEventStream events)
{
EnsureNotDisposed();
@@ -770,6 +813,34 @@ internal sealed class RuntimeSetPositionState : IDisposable
portal,
captureMoverPreparationAuthority: true);
+ internal void PrepareDormantLocalActivationOwnership(
+ RuntimeEntityRecord record,
+ PhysicsBody body,
+ in RuntimeEntityPlacementToken token)
+ {
+ EnsureNotDisposed();
+ ArgumentNullException.ThrowIfNull(record);
+ ArgumentNullException.ThrowIfNull(body);
+ if (!token.IsValid
+ || record.Key != token.Entity
+ || !_operations.TryGetValue(token.Entity, out Operation? operation)
+ || operation.Token != token
+ || operation.Stage is not RuntimeEntityPlacementStage
+ .AwaitingPreparation
+ || !ReferenceEquals(operation.Record, record)
+ || record.PhysicsBody is not null
+ || !IsCurrent(operation)
+ || body.InWorld
+ || (body.TransientState & TransientStateFlags.Active) != 0)
+ {
+ throw new InvalidOperationException(
+ "Dormant local activation must bind to the exact current placement owner.");
+ }
+
+ operation.Body = body;
+ operation.DormantLocalActivation = true;
+ }
+
private RuntimeEntityPlacementToken BeginAcceptedPlacementCore(
RuntimeEntityRecord record,
ulong expectedPositionAuthorityVersion,
@@ -2531,6 +2602,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
uint prefix,
bool includeOutdoorCells)
{
+ if (_physics.SpatialRootCount == 0)
+ return false;
var roots = new List();
_physics.CopySpatialRootsTo(roots);
for (int index = 0; index < roots.Count; index++)
@@ -2586,7 +2659,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
uint prefix = state.Token.LandblockPrefix;
foreach (Operation operation in _operations.Values)
{
- if (operation.WakeableLostCell)
+ if (operation.WakeableLostCell || operation.DormantLocalActivation)
continue;
if (PlacementTouchesPrefix(operation.Command.Physics, prefix)
|| ResultTouchesPrefix(operation.Result, prefix)
@@ -2703,7 +2776,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
|| !_collisionPrefixQuiescence.TryGetValue(
operation.CollisionPrefix,
out CollisionPrefixQuiescence? state)
- || !state.AbortReleaseInProgress)
+ || !state.ReleaseInProgress
+ || !state.ReleaseGenerationReady)
{
return;
}
@@ -2745,21 +2819,40 @@ internal sealed class RuntimeSetPositionState : IDisposable
private void RebindQuiescedDeferredOperations(
in RuntimeCollisionPrefixQuiescenceToken token,
ulong successorGeneration,
- bool ready)
+ bool ready,
+ bool releaseUnavailable = false)
{
foreach (Operation operation in _operations.Values.ToArray())
{
+ bool unavailableAfterReadyCommit = ready
+ && operation.CollisionQuiescenceHeld
+ && operation.CollisionGeneration == 0UL
+ && operation.CollisionPrefix == token.LandblockPrefix;
if (!operation.WakeableLostCell
- || operation.CollisionGeneration != token.CollisionGeneration
- || operation.CollisionPrefix != token.LandblockPrefix)
+ || !operation.CollisionQuiescenceHeld
+ || operation.CollisionPrefix != token.LandblockPrefix
+ || (operation.CollisionGeneration
+ != token.CollisionGeneration
+ && !unavailableAfterReadyCommit))
{
continue;
}
UnindexDeferred(operation);
- operation.CollisionGeneration = successorGeneration;
+ ulong reboundGeneration = releaseUnavailable
+ || unavailableAfterReadyCommit
+ ? 0UL
+ : successorGeneration;
+ operation.CollisionGeneration = reboundGeneration;
operation.CollisionGenerationReady = ready
- && successorGeneration != 0UL;
- if (successorGeneration != 0UL)
+ && reboundGeneration != 0UL;
+ if (releaseUnavailable || unavailableAfterReadyCommit)
+ {
+ operation.CollisionQuiescenceHeld = false;
+ operation.Stage = operation.RequiresPreparation
+ ? RuntimeEntityPlacementStage.AwaitingPreparation
+ : RuntimeEntityPlacementStage.AwaitingCell;
+ }
+ if (reboundGeneration != 0UL)
IndexDeferred(operation);
else
IndexUnboundDeferred(operation);
@@ -3073,26 +3166,27 @@ internal sealed class RuntimeSetPositionState : IDisposable
operation.Command.Physics,
out CollisionPrefixQuiescence? blocking))
{
- if (blocking!.AbortReleaseInProgress
+ if (blocking!.ReleaseInProgress
+ && blocking.ReleaseGenerationReady
&& operation.CollisionQuiescenceHeld
&& operation.CollisionPrefix
== blocking.Token.LandblockPrefix)
{
- // The old collision generation remains active. Keep the
- // admission barrier closed to new commands while this exact
- // parked operation restores and its Place receipt drains.
+ // The selected collision generation is active. Keep the
+ // admission barrier closed while this exact parked operation
+ // restores and its Place receipt drains.
restoringQuiescence = blocking.Token;
}
else
{
- UnindexDeferred(operation);
- operation.CollisionPrefix = blocking.Token.LandblockPrefix;
- operation.CollisionGeneration = blocking.Token.CollisionGeneration;
- operation.CollisionGenerationReady = false;
- operation.CollisionQuiescenceHeld = true;
- operation.Stage = RuntimeEntityPlacementStage.QuiescenceHeld;
- IndexDeferred(operation);
- return;
+ UnindexDeferred(operation);
+ operation.CollisionPrefix = blocking.Token.LandblockPrefix;
+ operation.CollisionGeneration = blocking.Token.CollisionGeneration;
+ operation.CollisionGenerationReady = false;
+ operation.CollisionQuiescenceHeld = true;
+ operation.Stage = RuntimeEntityPlacementStage.QuiescenceHeld;
+ IndexDeferred(operation);
+ return;
}
}
diff --git a/tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs b/tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs
index a5f9106d..a3287ea4 100644
--- a/tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs
+++ b/tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs
@@ -2,9 +2,13 @@ using System.Collections.Immutable;
using System.Numerics;
using System.Reflection;
using AcDream.App.Streaming;
+using AcDream.Core.Net;
+using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using AcDream.Runtime.Entities;
+using AcDream.Runtime.Physics;
+using AcDream.Runtime.Session;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
@@ -19,6 +23,91 @@ public sealed class LandblockPhysicsPublisherTests
private static readonly float[] HeightTable =
Enumerable.Range(0, 256).Select(index => (float)index).ToArray();
+ [Fact]
+ public void ReplacementYieldsAtHeldWithdrawAndPlaceWithoutPostEngineReseal()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(
+ new PhysicsDataCache());
+ RuntimePhysicsState physics = lifetime.Physics;
+ var publisher = new LandblockPhysicsPublisher(physics, HeightTable);
+ Publish(publisher, Build(FirstLandblock));
+
+ const uint guid = 0x70004101u;
+ const uint cell = 0xA9B40001u;
+ Vector3 position = new(10f, 10f, 0f);
+ RuntimeEntityRecord record = lifetime.RegisterEntity(
+ RuntimeSpawn(guid, cell, position)).Canonical!;
+ lifetime.Entities.SetFinalPhysicsState(record, PhysicsStateFlags.Gravity);
+ lifetime.Entities.SetFullCell(record, cell, FirstLandblock);
+ var body = new PhysicsBody
+ {
+ Position = position,
+ Orientation = Quaternion.Identity,
+ LastUpdateTime = 1d,
+ State = PhysicsStateFlags.Gravity,
+ TransientState = TransientStateFlags.Active,
+ };
+ body.SnapToCell(cell, position, position);
+ lifetime.Entities.SetPhysicsBody(record, body);
+ record.ObjectClock.Activate();
+ physics.AcknowledgeSpatialProjection(record, spatial: true);
+ RuntimePlacementProjectionToken seeded = SeedRuntimePlacement(
+ physics,
+ record,
+ cell,
+ position);
+ Assert.True(physics.SetPosition.AcknowledgeProjection(seeded));
+
+ LandblockPhysicsPublication receipt = Begin(
+ publisher,
+ Build(FirstLandblock));
+ Assert.False(publisher.CompletePublication(receipt));
+ Assert.False(receipt.EngineMutationCommitted);
+ Assert.True(physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot withdrawal));
+ Assert.Equal(RuntimePlacementProjectionKind.Withdraw, withdrawal.Kind);
+
+ Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
+ Assert.False(publisher.CompletePublication(receipt));
+ Assert.True(receipt.EngineMutationCommitted);
+ Assert.True(receipt.SealCommitted);
+ Assert.True(physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot placement));
+ Assert.Equal(RuntimePlacementProjectionKind.Place, placement.Kind);
+
+ Assert.False(publisher.CompletePublication(receipt));
+ Assert.True(receipt.SealCommitted);
+ Assert.True(receipt.EngineMutationCommitted);
+
+ Assert.True(physics.SetPosition.AcknowledgeProjection(placement.Token));
+ Assert.True(publisher.CompletePublication(receipt));
+ Assert.True(receipt.CompletionCommitted);
+
+ LandblockPhysicsPublication cancelled = Begin(
+ publisher,
+ Build(FirstLandblock));
+ Assert.False(publisher.CompletePublication(cancelled));
+ Assert.True(physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot cancelWithdrawal));
+ Assert.True(physics.SetPosition.AcknowledgeProjection(
+ cancelWithdrawal.Token));
+ Assert.False(publisher.CompletePublication(cancelled));
+ Assert.True(cancelled.EngineMutationCommitted);
+ Assert.True(cancelled.SealCommitted);
+ Assert.True(physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot cancelPlacement));
+
+ Assert.False(cancelled.TryCancel());
+ Assert.True(cancelled.CancellationRequested);
+ Assert.True(cancelled.SealCommitted);
+ Assert.True(physics.SetPosition.AcknowledgeProjection(
+ cancelPlacement.Token));
+ Assert.True(cancelled.TryCancel());
+ RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
+ Assert.Equal(0, ownership.CollisionPrefixMutationCount);
+ Assert.Equal(0, ownership.CollisionAdmissionCount);
+ }
+
[Fact]
public void Constructor_ClonesHeightTableAndRejectsIncompleteInput()
{
@@ -821,6 +910,151 @@ public sealed class LandblockPhysicsPublisherTests
LandblockBuild build) =>
publisher.BeginPublication(RenderReceipt(build));
+ private static RuntimePlacementProjectionToken SeedRuntimePlacement(
+ RuntimePhysicsState physics,
+ RuntimeEntityRecord record,
+ uint cell,
+ Vector3 position)
+ {
+ Type coreAssemblyMarker = typeof(PhysicsEngine);
+ Type requestType = coreAssemblyMarker.Assembly.GetType(
+ "AcDream.Core.Physics.PhysicsSetPositionRequest",
+ throwOnError: true)!;
+ Type flagsType = coreAssemblyMarker.Assembly.GetType(
+ "AcDream.Core.Physics.PhysicsSetPositionFlags",
+ throwOnError: true)!;
+ Type placementClassType = coreAssemblyMarker.Assembly.GetType(
+ "AcDream.Core.Physics.PhysicsPlacementClass",
+ throwOnError: true)!;
+ object request = Activator.CreateInstance(
+ requestType,
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+ binder: null,
+ args:
+ [
+ position,
+ Quaternion.Identity,
+ cell,
+ position,
+ ImmutableArray.Empty,
+ 1f,
+ 0.4f,
+ 0.4f,
+ PhysicsStateFlags.None,
+ ObjectInfoState.None,
+ 0u,
+ Enum.ToObject(placementClassType, 0),
+ Enum.ToObject(flagsType, 0x011u),
+ Vector3.Zero,
+ 0f,
+ 0f,
+ 0u,
+ cell,
+ ],
+ culture: null)!;
+
+ Type runtimeAssemblyMarker = typeof(RuntimePhysicsState);
+ Type commandType = runtimeAssemblyMarker.Assembly.GetType(
+ "AcDream.Runtime.Physics.RuntimeSetPositionCommand",
+ throwOnError: true)!;
+ Type kindType = runtimeAssemblyMarker.Assembly.GetType(
+ "AcDream.Runtime.Physics.RuntimeSetPositionOperationKind",
+ throwOnError: true)!;
+ object command = Activator.CreateInstance(
+ commandType,
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+ binder: null,
+ args:
+ [
+ request,
+ Enum.ToObject(kindType, 2),
+ 10d,
+ 0UL,
+ 0f,
+ 0f,
+ default(RuntimePortalPlacementAuthority),
+ ],
+ culture: null)!;
+ MethodInfo apply = physics.SetPosition.GetType().GetMethod(
+ "Apply",
+ BindingFlags.Instance | BindingFlags.NonPublic)
+ ?? throw new MissingMethodException("Runtime SetPosition.Apply");
+ object outcome = apply.Invoke(
+ physics.SetPosition,
+ [record, record.PositionAuthorityVersion, command])!;
+ return (RuntimePlacementProjectionToken)(outcome.GetType().GetProperty(
+ "Projection",
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
+ ?.GetValue(outcome)
+ ?? throw new MissingMemberException("Runtime placement projection"));
+ }
+
+ private static WorldSession.EntitySpawn RuntimeSpawn(
+ uint guid,
+ uint cell,
+ Vector3 position)
+ {
+ var serverPosition = new CreateObject.ServerPosition(
+ cell,
+ position.X,
+ position.Y,
+ position.Z,
+ 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: 1);
+ var spawnPhysics = new PhysicsSpawnData(
+ RawState: (uint)PhysicsStateFlags.Gravity,
+ 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(),
+ Array.Empty(),
+ Array.Empty(),
+ null,
+ null,
+ "app-collision-publication-fixture",
+ null,
+ null,
+ 0x09000001u,
+ PhysicsState: (uint)PhysicsStateFlags.Gravity,
+ InstanceSequence: 1,
+ MovementSequence: 1,
+ ServerControlSequence: 1,
+ PositionSequence: 1,
+ Physics: spawnPhysics);
+ }
+
private static LandblockRenderPublication RenderReceipt(LandblockBuild build)
{
var publisher = new LandblockRenderPublisher(
diff --git a/tests/AcDream.App.Tests/Streaming/LandblockRetirementCoordinatorTests.cs b/tests/AcDream.App.Tests/Streaming/LandblockRetirementCoordinatorTests.cs
index 5904dddb..3c8c84fe 100644
--- a/tests/AcDream.App.Tests/Streaming/LandblockRetirementCoordinatorTests.cs
+++ b/tests/AcDream.App.Tests/Streaming/LandblockRetirementCoordinatorTests.cs
@@ -9,6 +9,106 @@ namespace AcDream.App.Tests.Streaming;
public sealed class LandblockRetirementCoordinatorTests
{
+ [Fact]
+ public void PendingPhysicsStageYieldsBeforeTerrainAndRemainsRetryable()
+ {
+ const uint landblockId = 0x2020FFFFu;
+ var ticket = new LandblockRetirementTicket(
+ new GpuLandblockRetirement(
+ landblockId,
+ LandblockRetirementKind.Full,
+ Array.Empty()),
+ LandblockRetirementStage.Physics
+ | LandblockRetirementStage.Terrain);
+ int physicsPolls = 0;
+ int terrainPolls = 0;
+
+ Assert.Equal(
+ LandblockRetirementOperationResult.Pending,
+ ticket.RunOnceStep(
+ LandblockRetirementStage.Physics,
+ () =>
+ {
+ physicsPolls++;
+ return false;
+ }));
+ Assert.Equal(LandblockRetirementStage.Physics, ticket.NextIncompleteStage);
+ Assert.Equal(1, physicsPolls);
+ Assert.Equal(0, terrainPolls);
+
+ Assert.Equal(
+ LandblockRetirementOperationResult.Progressed,
+ ticket.RunOnceStep(
+ LandblockRetirementStage.Physics,
+ () =>
+ {
+ physicsPolls++;
+ return true;
+ }));
+ Assert.Equal(LandblockRetirementStage.Terrain, ticket.NextIncompleteStage);
+ Assert.True(ticket.RunOnce(
+ LandblockRetirementStage.Terrain,
+ () => terrainPolls++));
+ Assert.True(ticket.IsComplete);
+ Assert.Equal(2, physicsPolls);
+ Assert.Equal(1, terrainPolls);
+ }
+
+ [Fact]
+ public void BudgetedPendingPhysicsPollsExactlyOncePerAdvance()
+ {
+ const uint landblockId = 0x2023FFFFu;
+ var state = StateWith(landblockId);
+ int physicsPolls = 0;
+ int terrainPolls = 0;
+ bool acknowledgePhysics = false;
+ LandblockRetirementOperationResult AdvancePresentation(
+ LandblockRetirementTicket ticket) =>
+ ticket.NextIncompleteStage switch
+ {
+ LandblockRetirementStage.Physics => ticket.RunOnceStep(
+ LandblockRetirementStage.Physics,
+ () =>
+ {
+ physicsPolls++;
+ return acknowledgePhysics;
+ }),
+ LandblockRetirementStage.Terrain => ticket.RunOnceStep(
+ LandblockRetirementStage.Terrain,
+ () => terrainPolls++),
+ { } stage => ticket.RunOnceStep(stage, () => { }),
+ };
+ LandblockRetirementCoordinator coordinator =
+ LandblockRetirementCoordinator.CreateBudgeted(
+ state,
+ AdvancePresentation,
+ ticket =>
+ {
+ while (!ticket.IsComplete)
+ _ = AdvancePresentation(ticket);
+ });
+ coordinator.BeginFull(landblockId);
+
+ var first = new StreamingWorkMeter(Budget(maxEntityOperations: 64));
+ coordinator.Advance(first);
+ first.FinishFrame();
+
+ Assert.Equal(1, physicsPolls);
+ Assert.Equal(0, terrainPolls);
+ Assert.Equal(0, first.Snapshot.FailureCount);
+ Assert.Equal(1, coordinator.PendingCount);
+
+ acknowledgePhysics = true;
+ var second = new StreamingWorkMeter(Budget(maxEntityOperations: 64));
+ coordinator.Advance(second);
+ second.FinishFrame();
+
+ Assert.Equal(2, physicsPolls);
+ Assert.Equal(1, terrainPolls);
+ Assert.Equal(0, second.Snapshot.FailureCount);
+ Assert.Equal(0, coordinator.PendingCount);
+ }
+
[Fact]
public void EntityStageFailure_DetachesImmediately_AndResumesAtFailedEntity()
{
diff --git a/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs b/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs
index 7bcb7114..131b4b6e 100644
--- a/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs
+++ b/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs
@@ -1,6 +1,8 @@
using System.Buffers.Binary;
+using System.Collections.Immutable;
using System.Net;
using System.Numerics;
+using System.Reflection;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
@@ -605,7 +607,7 @@ public sealed class HeadlessSessionHostTests
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu;
- _ = HeadlessCollisionGenerationTransaction.Execute(
+ CompleteCollisionGeneration(
physics,
landblockId,
afterAdmission: null,
@@ -616,7 +618,7 @@ public sealed class HeadlessSessionHostTests
CollisionAssets(landblockId, 10f)));
Assert.Throws(() =>
- HeadlessCollisionGenerationTransaction.Execute(
+ CompleteCollisionGeneration(
physics,
landblockId,
_ => throw new FixtureCollisionPublicationException(),
@@ -629,13 +631,137 @@ public sealed class HeadlessSessionHostTests
Assert.Equal(0, ownership.CollisionAdmissionCount);
}
+ [Fact]
+ public void CollisionTransactionYieldsTheFirstNonterminalRuntimePoll()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime();
+ RuntimePhysicsState physics = lifetime.Physics;
+ const uint landblockId = 0xA9B4FFFFu;
+ HeadlessCollisionGenerationTransaction transaction =
+ HeadlessCollisionGenerationTransaction.Begin(
+ physics,
+ landblockId,
+ afterAdmission: null,
+ (admission, prepared) =>
+ physics.StageCollisionAssets(
+ admission,
+ prepared,
+ CollisionAssets(landblockId, 10f)));
+
+ HeadlessCollisionGenerationAdvance advance;
+ do
+ {
+ advance = transaction.Advance();
+ Assert.True(advance.Progressed);
+ }
+ while (!advance.YieldToCaller);
+
+ Assert.False(advance.Completed);
+ Assert.False(advance.WaitingForProjectionAcknowledgement);
+ Assert.False(transaction.CompletionCommitted);
+
+ do
+ {
+ advance = transaction.Advance();
+ Assert.True(advance.Progressed);
+ }
+ while (!advance.Completed && !advance.YieldToCaller);
+ if (!advance.Completed)
+ advance = transaction.Advance();
+
+ Assert.True(advance.Completed);
+ Assert.True(transaction.EngineMutationCommitted);
+ Assert.True(transaction.CompletionCommitted);
+ Assert.Equal(0, physics.CaptureOwnership().CollisionAdmissionCount);
+ }
+
+ [Fact]
+ public void CollisionTransactionRetainsPostEngineCancellationUntilPlaceAck()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime();
+ RuntimePhysicsState physics = lifetime.Physics;
+ const uint landblockId = 0xA9B4FFFFu;
+ CompleteCollisionGeneration(
+ physics,
+ landblockId,
+ afterAdmission: null,
+ (admission, prepared) =>
+ physics.StageCollisionAssets(
+ admission,
+ prepared,
+ CollisionAssets(landblockId, 10f)));
+
+ const uint guid = 0x70004201u;
+ const uint cell = 0xA9B40001u;
+ Vector3 position = new(10f, 10f, 0f);
+ RuntimeEntityRecord record = lifetime.RegisterEntity(
+ Spawn(guid)).Canonical!;
+ lifetime.Entities.SetFinalPhysicsState(record, PhysicsStateFlags.Gravity);
+ lifetime.Entities.SetFullCell(record, cell, landblockId);
+ var body = new PhysicsBody
+ {
+ Position = position,
+ Orientation = Quaternion.Identity,
+ LastUpdateTime = 1d,
+ State = PhysicsStateFlags.Gravity,
+ TransientState = TransientStateFlags.Active,
+ };
+ body.SnapToCell(cell, position, position);
+ lifetime.Entities.SetPhysicsBody(record, body);
+ record.ObjectClock.Activate();
+ physics.AcknowledgeSpatialProjection(record, spatial: true);
+ RuntimePlacementProjectionToken seeded = SeedRuntimePlacement(
+ physics,
+ record,
+ cell,
+ position);
+ Assert.True(physics.SetPosition.AcknowledgeProjection(seeded));
+
+ HeadlessCollisionGenerationTransaction transaction =
+ HeadlessCollisionGenerationTransaction.Begin(
+ physics,
+ landblockId,
+ afterAdmission: null,
+ (admission, prepared) =>
+ physics.StageCollisionAssets(
+ admission,
+ prepared,
+ CollisionAssets(landblockId, 20f)));
+ HeadlessCollisionGenerationAdvance advance;
+ do
+ {
+ advance = transaction.Advance();
+ }
+ while (!advance.WaitingForProjectionAcknowledgement);
+ Assert.False(transaction.EngineMutationCommitted);
+ Assert.True(physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot withdrawal));
+ Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
+
+ do
+ {
+ advance = transaction.Advance();
+ }
+ while (!advance.WaitingForProjectionAcknowledgement);
+ Assert.True(transaction.EngineMutationCommitted);
+ Assert.True(physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot placement));
+
+ Assert.False(transaction.TryCancel());
+ Assert.True(physics.SetPosition.AcknowledgeProjection(placement.Token));
+ Assert.True(transaction.TryCancel());
+ RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
+ Assert.Equal(0, ownership.CollisionPrefixMutationCount);
+ Assert.Equal(0, ownership.CollisionAdmissionCount);
+ }
+
[Fact]
public void CollisionTransactionCancelsStagingFaultWithoutWithdrawingActiveWorld()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu;
- _ = HeadlessCollisionGenerationTransaction.Execute(
+ CompleteCollisionGeneration(
physics,
landblockId,
afterAdmission: null,
@@ -646,7 +772,7 @@ public sealed class HeadlessSessionHostTests
CollisionAssets(landblockId, 10f)));
Assert.Throws(() =>
- HeadlessCollisionGenerationTransaction.Execute(
+ CompleteCollisionGeneration(
physics,
landblockId,
afterAdmission: null,
@@ -714,6 +840,107 @@ public sealed class HeadlessSessionHostTests
runtime.MovementOwner.Controller = controller;
}
+ private static void CompleteCollisionGeneration(
+ RuntimePhysicsState physics,
+ uint landblockId,
+ Action? afterAdmission,
+ Action stage)
+ {
+ HeadlessCollisionGenerationTransaction transaction =
+ HeadlessCollisionGenerationTransaction.Begin(
+ physics,
+ landblockId,
+ afterAdmission,
+ stage);
+ while (true)
+ {
+ HeadlessCollisionGenerationAdvance advance = transaction.Advance();
+ if (advance.Completed)
+ return;
+ Assert.True(advance.Progressed);
+ Assert.False(advance.WaitingForProjectionAcknowledgement);
+ }
+ }
+
+ private static RuntimePlacementProjectionToken SeedRuntimePlacement(
+ RuntimePhysicsState physics,
+ RuntimeEntityRecord record,
+ uint cell,
+ Vector3 position)
+ {
+ Type coreMarker = typeof(PhysicsEngine);
+ Type requestType = coreMarker.Assembly.GetType(
+ "AcDream.Core.Physics.PhysicsSetPositionRequest",
+ throwOnError: true)!;
+ Type flagsType = coreMarker.Assembly.GetType(
+ "AcDream.Core.Physics.PhysicsSetPositionFlags",
+ throwOnError: true)!;
+ Type placementClassType = coreMarker.Assembly.GetType(
+ "AcDream.Core.Physics.PhysicsPlacementClass",
+ throwOnError: true)!;
+ object request = Activator.CreateInstance(
+ requestType,
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+ binder: null,
+ args:
+ [
+ position,
+ Quaternion.Identity,
+ cell,
+ position,
+ ImmutableArray.Empty,
+ 1f,
+ 0.4f,
+ 0.4f,
+ PhysicsStateFlags.None,
+ ObjectInfoState.None,
+ 0u,
+ Enum.ToObject(placementClassType, 0),
+ Enum.ToObject(flagsType, 0x011u),
+ Vector3.Zero,
+ 0f,
+ 0f,
+ 0u,
+ cell,
+ ],
+ culture: null)!;
+ Type runtimeMarker = typeof(RuntimePhysicsState);
+ Type commandType = runtimeMarker.Assembly.GetType(
+ "AcDream.Runtime.Physics.RuntimeSetPositionCommand",
+ throwOnError: true)!;
+ Type kindType = runtimeMarker.Assembly.GetType(
+ "AcDream.Runtime.Physics.RuntimeSetPositionOperationKind",
+ throwOnError: true)!;
+ object command = Activator.CreateInstance(
+ commandType,
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+ binder: null,
+ args:
+ [
+ request,
+ Enum.ToObject(kindType, 2),
+ 10d,
+ 0UL,
+ 0f,
+ 0f,
+ default(RuntimePortalPlacementAuthority),
+ ],
+ culture: null)!;
+ MethodInfo apply = physics.SetPosition.GetType().GetMethod(
+ "Apply",
+ BindingFlags.Instance | BindingFlags.NonPublic)
+ ?? throw new MissingMethodException("Runtime SetPosition.Apply");
+ object outcome = apply.Invoke(
+ physics.SetPosition,
+ [record, record.PositionAuthorityVersion, command])!;
+ return (RuntimePlacementProjectionToken)(outcome.GetType().GetProperty(
+ "Projection",
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
+ ?.GetValue(outcome)
+ ?? throw new MissingMemberException("Runtime placement projection"));
+ }
+
private static RuntimeLandblockCollisionAssets CollisionAssets(
uint landblockId,
float terrainHeight)
diff --git a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerPhysicsPublicationStateTests.cs b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerPhysicsPublicationStateTests.cs
index 1768124f..0c432e83 100644
--- a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerPhysicsPublicationStateTests.cs
+++ b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerPhysicsPublicationStateTests.cs
@@ -66,7 +66,33 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
.IsExactPreparedPlacementCurrent(
fixture.Record,
fixture.Placement,
- fixture.Command));
+ fixture.Command));
+ }
+
+ [Fact]
+ public void DormantOwnershipPreflightFailurePublishesNoCanonicalOwner()
+ {
+ using var fixture = new Fixture();
+ RuntimeLocalPlayerPhysicsPublicationToken publication =
+ fixture.Prepare();
+ Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
+ fixture.Record,
+ publishWithdrawal: false));
+ var unownedBody = new PhysicsBody();
+
+ Assert.Throws(() => fixture.Lifetime.Physics
+ .SetPosition.PrepareDormantLocalActivationOwnership(
+ fixture.Record,
+ unownedBody,
+ publication.Placement));
+
+ Assert.Null(fixture.Record.PhysicsBody);
+ Assert.Null(fixture.Movement.Controller);
+ Assert.False(unownedBody.InWorld);
+ Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
+ Assert.Equal(1, fixture.Owner.CaptureOwnership().CandidateCount);
+ Assert.Equal(0, fixture.Owner.CaptureOwnership()
+ .PendingActivationCount);
}
[Fact]
@@ -2467,32 +2493,48 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
- while (true)
+ bool engineCommitted = false;
+ for (int poll = 0; poll < 10_000; poll++)
{
- while (!physics.AdvanceCollisionRetainedOwnerCapture(
- admission,
- prepared).Completed)
+ if (!engineCommitted)
{
+ 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)
+ continue;
}
- foreach (uint ownerId in prepared.RetainedOwnerIds)
+ RuntimeCollisionGenerationCommit result =
+ physics.CommitCollisionGeneration(admission, prepared);
+ if (result.Completed)
+ return result;
+ while (physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot projection))
{
- physics.RefreshCollisionRetainedOwner(
- admission,
- prepared,
- ownerId);
+ Assert.True(physics.SetPosition.AcknowledgeProjection(
+ projection.Token));
}
- RuntimeCollisionSealStep seal;
- do
- {
- seal = physics.AdvanceCollisionGenerationSeal(
- admission,
- prepared);
- }
- while (!seal.Completed && !seal.Restarted);
- if (seal.Completed)
- break;
+ engineCommitted = result.EngineCommitted;
}
- return physics.CommitCollisionGeneration(admission, prepared);
+ throw new InvalidOperationException(
+ "Collision generation did not complete its Runtime mutation transaction.");
}
private static RuntimeLandblockCollisionAssets CollisionAssets(
diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionMutationTransactionTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionMutationTransactionTests.cs
new file mode 100644
index 00000000..090a5f29
--- /dev/null
+++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionMutationTransactionTests.cs
@@ -0,0 +1,479 @@
+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(),
+ Array.Empty(),
+ 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];
+ }
+ }
+}
diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs
index b49bc996..a244db9c 100644
--- a/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs
+++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs
@@ -10,7 +10,7 @@ using DatReaderWriter.Types;
namespace AcDream.Runtime.Tests.Physics;
-public sealed class RuntimeCollisionPrefixQuiescenceTests
+public sealed partial class RuntimeCollisionPrefixQuiescenceTests
{
private const uint PrefixP = 0xA9B40000u;
private const uint CellP = PrefixP | 0x0001u;
@@ -68,7 +68,10 @@ public sealed class RuntimeCollisionPrefixQuiescenceTests
Assert.True(fixture.Lifetime.Physics
.IsCollisionPrefixMutationPermissionCurrent(permission));
Assert.True(fixture.Lifetime.Physics
- .CompleteCollisionPrefixQuiescence(permission));
+ .CancelCollisionPrefixQuiescence(
+ token,
+ successorGeneration: 2UL,
+ successorReady: false));
Assert.False(fixture.Lifetime.Physics
.IsCollisionPrefixMutationPermissionCurrent(permission));
}
diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsOwnershipBoundaryTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsOwnershipBoundaryTests.cs
new file mode 100644
index 00000000..3795a3ba
--- /dev/null
+++ b/tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsOwnershipBoundaryTests.cs
@@ -0,0 +1,44 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using AcDream.Core.Physics;
+
+namespace AcDream.Runtime.Tests.Physics;
+
+public sealed class RuntimePhysicsOwnershipBoundaryTests
+{
+ [Fact]
+ public void PhysicsWorldRootMutatorsAreRuntimeOnlyProductionInternals()
+ {
+ string[] rootMutators =
+ [
+ nameof(PhysicsEngine.AddLandblock),
+ nameof(PhysicsEngine.RemoveLandblock),
+ nameof(PhysicsEngine.DemoteLandblockToTerrain),
+ nameof(PhysicsEngine.Clear),
+ ];
+ Type engine = typeof(PhysicsEngine);
+ MethodInfo[] publicMethods = engine.GetMethods(
+ BindingFlags.Instance | BindingFlags.Public);
+ MethodInfo[] internalMethods = engine.GetMethods(
+ BindingFlags.Instance | BindingFlags.NonPublic);
+ foreach (string name in rootMutators)
+ {
+ Assert.DoesNotContain(
+ publicMethods,
+ method => method.Name == name);
+ Assert.Contains(
+ internalMethods,
+ method => method.Name == name);
+ }
+
+ string[] friends = engine.Assembly
+ .GetCustomAttributes()
+ .Select(attribute => attribute.AssemblyName.Split(',')[0])
+ .ToArray();
+ Assert.Contains("AcDream.Runtime", friends);
+ Assert.DoesNotContain("AcDream.App", friends);
+ Assert.DoesNotContain("acdream-headless", friends);
+ Assert.Contains("AcDream.App.Tests", friends);
+ Assert.Contains("AcDream.Headless.Tests", friends);
+ }
+}
diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs
index 0972e99a..78349ead 100644
--- a/tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs
+++ b/tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs
@@ -336,7 +336,8 @@ public sealed class RuntimePhysicsStateTests
CommitPrepared(first.Physics, newer, prepared));
RuntimeCollisionAcknowledgement withdrawn =
- first.Physics.WithdrawCollision(0xA9B4FFFFu);
+ CompleteWithdrawal(first.Physics, 0xA9B4FFFFu)
+ .Acknowledgement;
Assert.True(withdrawn.WasResident);
Assert.True(withdrawn.Generation > completed.Generation);
Assert.Equal(0, first.Physics.Engine.LandblockCount);
@@ -465,7 +466,8 @@ public sealed class RuntimePhysicsStateTests
uint owner = Assert.Single(SealPrepared(physics, admission, prepared));
physics.Engine.ShadowObjects.UpdatePhysicsState(owner, 0x14u);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f));
@@ -561,6 +563,11 @@ public sealed class RuntimePhysicsStateTests
Assert.True(sealSteps > ownerCount);
Assert.True(workUnits > ownerCount);
+ Assert.False(physics.CommitCollisionGeneration(
+ admission,
+ prepared).Committed);
+ _ = SealPrepared(physics, admission, prepared);
+
_ = GC.GetAllocatedBytesForCurrentThread();
long before = GC.GetAllocatedBytesForCurrentThread();
RuntimeCollisionGenerationCommit commit =
@@ -662,7 +669,8 @@ public sealed class RuntimePhysicsStateTests
}
while (!seal.Completed);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f));
@@ -742,7 +750,8 @@ public sealed class RuntimePhysicsStateTests
break;
}
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
ShadowEntry[] entries = physics.Engine.ShadowObjects
@@ -841,7 +850,8 @@ public sealed class RuntimePhysicsStateTests
}
while (!seal.Completed);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
ShadowEntry[] entries = physics.Engine.ShadowObjects
@@ -885,6 +895,10 @@ public sealed class RuntimePhysicsStateTests
ShadowObjectRegistry shadowFacade = physics.Engine.ShadowObjects;
int notifications = 0;
physics.CollisionGenerationCommitted += _ => notifications++;
+ Assert.False(physics.CommitCollisionGeneration(
+ admission,
+ prepared).Committed);
+ _ = SealPrepared(physics, admission, prepared);
_ = GC.GetAllocatedBytesForCurrentThread();
long before = GC.GetAllocatedBytesForCurrentThread();
RuntimeCollisionGenerationCommit commit =
@@ -1049,6 +1063,10 @@ public sealed class RuntimePhysicsStateTests
PhysicsDataCache cacheFacade = physics.DataCache;
CellGraph graphFacade = physics.DataCache.CellGraph;
ShadowObjectRegistry shadowFacade = physics.Engine.ShadowObjects;
+ Assert.False(physics.CommitCollisionGeneration(
+ admission,
+ prepared).Committed);
+ _ = SealPrepared(physics, admission, prepared);
_ = GC.GetAllocatedBytesForCurrentThread();
long before = GC.GetAllocatedBytesForCurrentThread();
RuntimeCollisionGenerationCommit commit =
@@ -1106,9 +1124,15 @@ public sealed class RuntimePhysicsStateTests
_ = SealPrepared(physics, firstAdmission, first);
_ = SealPrepared(physics, secondAdmission, second);
+ Assert.False(physics.CommitCollisionGeneration(
+ firstAdmission,
+ first).Committed);
+ _ = SealPrepared(physics, firstAdmission, first);
+
_ = GC.GetAllocatedBytesForCurrentThread();
long firstBefore = GC.GetAllocatedBytesForCurrentThread();
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
firstAdmission,
first).Committed);
long firstAllocated =
@@ -1118,10 +1142,15 @@ public sealed class RuntimePhysicsStateTests
Assert.True(physics.Engine.IsLandblockTerrainResident(firstLandblock));
Assert.False(physics.Engine.IsLandblockTerrainResident(secondLandblock));
+ _ = SealPrepared(physics, secondAdmission, second);
+ Assert.False(physics.CommitCollisionGeneration(
+ secondAdmission,
+ second).Committed);
_ = SealPrepared(physics, secondAdmission, second);
_ = GC.GetAllocatedBytesForCurrentThread();
long secondBefore = GC.GetAllocatedBytesForCurrentThread();
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
secondAdmission,
second).Committed);
long secondAllocated =
@@ -1172,7 +1201,8 @@ public sealed class RuntimePhysicsStateTests
_ = SealPrepared(physics, firstAdmission, first);
_ = SealPrepared(physics, secondAdmission, second);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
firstAdmission,
first).Committed);
physics.Engine.ShadowObjects.UpdatePosition(
@@ -1185,7 +1215,8 @@ public sealed class RuntimePhysicsStateTests
seedCellId: 0x01010001u);
_ = SealPrepared(physics, secondAdmission, second);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
secondAdmission,
second).Committed);
Assert.Equal(12f, Assert.Single(
@@ -1256,11 +1287,13 @@ public sealed class RuntimePhysicsStateTests
_ = SealPrepared(physics, northAdmission, northPrepared);
_ = SealPrepared(physics, southAdmission, southPrepared);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
northAdmission,
northPrepared).Committed);
_ = SealPrepared(physics, southAdmission, southPrepared);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
southAdmission,
southPrepared).Committed);
Assert.True(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
@@ -1394,7 +1427,8 @@ public sealed class RuntimePhysicsStateTests
physics.Engine.UpdatePlayerCurrCell(secondCell);
ObjCell oldRootCell = physics.DataCache.CellGraph.CurrCell!;
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
@@ -1454,7 +1488,8 @@ public sealed class RuntimePhysicsStateTests
destination,
seedCellId: 0x02020001u);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
Assert.False(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
@@ -1521,7 +1556,8 @@ public sealed class RuntimePhysicsStateTests
seedCellId: 0x02020001u,
isStatic: false);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
ShadowEntry entry = Assert.Single(
@@ -1692,8 +1728,8 @@ public sealed class RuntimePhysicsStateTests
preparedThird,
CollisionAssets(replacement));
- Assert.True(physics.DemoteCollisionToTerrain(demoted).WasResident);
- Assert.True(physics.WithdrawCollision(withdrawn).WasResident);
+ Assert.True(CompleteDemotion(physics, demoted).WasResident);
+ Assert.True(CompleteWithdrawal(physics, withdrawn).WasResident);
Assert.True(CommitPrepared(
physics,
third,
@@ -1745,7 +1781,8 @@ public sealed class RuntimePhysicsStateTests
}
_ = SealPrepared(physics, retiredAdmission, retiredPrepared);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
retiredAdmission,
retiredPrepared).Committed);
if (beginRebase)
@@ -1762,9 +1799,9 @@ public sealed class RuntimePhysicsStateTests
}
if (withdraw)
- Assert.True(physics.WithdrawCollision(retired).WasResident);
+ Assert.True(CompleteWithdrawal(physics, retired).WasResident);
else
- Assert.True(physics.DemoteCollisionToTerrain(retired).WasResident);
+ Assert.True(CompleteDemotion(physics, retired).WasResident);
Assert.True(CommitPrepared(
physics,
@@ -1986,6 +2023,11 @@ public sealed class RuntimePhysicsStateTests
while (!seal.Completed);
Assert.Equal(ownerCount, worked);
+ Assert.False(physics.CommitCollisionGeneration(
+ admission,
+ prepared).Committed);
+ _ = SealPrepared(physics, admission, prepared);
+
_ = GC.GetAllocatedBytesForCurrentThread();
long before = GC.GetAllocatedBytesForCurrentThread();
RuntimeCollisionGenerationCommit commit =
@@ -2050,7 +2092,8 @@ public sealed class RuntimePhysicsStateTests
survivingTarget,
seedCellId: 0x02020001u,
isStatic: false);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
survivingAdmission,
surviving).Committed);
Assert.Contains(
@@ -2097,7 +2140,8 @@ public sealed class RuntimePhysicsStateTests
0f,
target,
seedCellId: 0x01010001u);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
Assert.True(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
@@ -2146,7 +2190,8 @@ public sealed class RuntimePhysicsStateTests
outside,
seedCellId: 0x02020001u);
physics.Engine.ShadowObjects.UpdatePhysicsState(ownerId, 0x55u);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
ShadowEntry entry = Assert.Single(
@@ -2217,7 +2262,7 @@ public sealed class RuntimePhysicsStateTests
replacementAdmission,
replacementPrepared);
- Assert.True(physics.WithdrawCollision(retired).WasResident);
+ Assert.True(CompleteWithdrawal(physics, retired).WasResident);
Assert.False(physics.CommitCollisionGeneration(
replacementAdmission,
replacementPrepared).Committed);
@@ -2226,7 +2271,8 @@ public sealed class RuntimePhysicsStateTests
physics,
replacementAdmission,
replacementPrepared);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
replacementAdmission,
replacementPrepared).Committed);
Assert.False(physics.Engine.IsLandblockTerrainResident(retired));
@@ -2254,7 +2300,8 @@ public sealed class RuntimePhysicsStateTests
uint ordinal = index + 0x1000u;
uint x = ordinal & 0xFFu;
uint y = (ordinal >> 8) & 0xFFu;
- _ = physics.WithdrawCollision(
+ _ = CompleteWithdrawal(
+ physics,
(x << 24) | (y << 16) | 0xFFFFu);
}
Assert.False(physics.CommitCollisionGeneration(
@@ -2272,7 +2319,8 @@ public sealed class RuntimePhysicsStateTests
Assert.True(++steps < 100_000);
}
while (!seal.Completed);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
}
@@ -2380,7 +2428,8 @@ public sealed class RuntimePhysicsStateTests
Assert.InRange(seal.WorkUnits, 0, 1);
}
while (!seal.Completed);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug());
@@ -2448,7 +2497,8 @@ public sealed class RuntimePhysicsStateTests
seedCellId: 0x01010001u,
isStatic: false);
physics.Engine.ShadowObjects.Deregister(77u);
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug());
@@ -2484,7 +2534,8 @@ public sealed class RuntimePhysicsStateTests
seedCellId: 0x01010001u,
isStatic: false);
}
- Assert.True(physics.CommitCollisionGeneration(
+ Assert.True(CompleteSealedCommit(
+ physics,
admission,
prepared).Committed);
Assert.Equal(
@@ -2903,7 +2954,55 @@ public sealed class RuntimePhysicsStateTests
PreparedLandblockCollisionGeneration prepared)
{
_ = SealPrepared(physics, admission, prepared);
- return physics.CommitCollisionGeneration(admission, prepared);
+ return CompleteSealedCommit(physics, admission, prepared);
+ }
+
+ private static RuntimeCollisionGenerationCommit CompleteSealedCommit(
+ RuntimePhysicsState physics,
+ RuntimeCollisionAdmission admission,
+ PreparedLandblockCollisionGeneration prepared)
+ {
+ for (int poll = 0; poll < 10_000; poll++)
+ {
+ RuntimeCollisionGenerationCommit commit =
+ physics.CommitCollisionGeneration(admission, prepared);
+ if (commit.Committed)
+ return commit;
+ if (!commit.EngineCommitted)
+ _ = SealPrepared(physics, admission, prepared);
+ }
+ throw new InvalidOperationException(
+ "Collision generation did not complete its Runtime mutation transaction.");
+ }
+
+ private static RuntimeCollisionMutationResult CompleteDemotion(
+ RuntimePhysicsState physics,
+ uint landblockId)
+ {
+ for (int poll = 0; poll < 10_000; poll++)
+ {
+ RuntimeCollisionMutationResult result =
+ physics.DemoteCollisionToTerrain(landblockId);
+ if (result.Completed)
+ return result;
+ }
+ throw new InvalidOperationException(
+ "Collision demotion did not complete its Runtime mutation transaction.");
+ }
+
+ private static RuntimeCollisionMutationResult CompleteWithdrawal(
+ RuntimePhysicsState physics,
+ uint landblockId)
+ {
+ for (int poll = 0; poll < 10_000; poll++)
+ {
+ RuntimeCollisionMutationResult result =
+ physics.WithdrawCollision(landblockId);
+ if (result.Completed)
+ return result;
+ }
+ throw new InvalidOperationException(
+ "Collision withdrawal did not complete its Runtime mutation transaction.");
}
private static uint[] SealPrepared(
diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs
index 92368bdd..aa7896e1 100644
--- a/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs
+++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs
@@ -1750,8 +1750,6 @@ public sealed class RuntimeSetPositionStateTests
RuntimePlacementProjectionSnapshot placed = observer.Deltas[^1].Placement;
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
Assert.Equal(DestinationIndoorCell, placed.Token.ExactCellId);
- Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
- placed.Token));
RuntimePhysicsOwnershipSnapshot final = lifetime.Physics.CaptureOwnership();
Assert.Equal(0, final.DeferredSetPositionBucketCount);
Assert.Equal(0, final.UnboundDeferredSetPositionCellCount);
@@ -1872,10 +1870,6 @@ public sealed class RuntimeSetPositionStateTests
Assert.All(observer.Deltas, delta => Assert.Equal(
RuntimePlacementProjectionKind.Place,
delta.Placement.Kind));
- Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
- observer.Deltas[0].Placement.Token));
- Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
- observer.Deltas[1].Placement.Token));
}
[Theory]
@@ -2425,32 +2419,48 @@ public sealed class RuntimeSetPositionStateTests
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
- while (true)
+ bool engineCommitted = false;
+ for (int poll = 0; poll < 10_000; poll++)
{
- while (!physics.AdvanceCollisionRetainedOwnerCapture(
- admission,
- prepared).Completed)
+ if (!engineCommitted)
{
+ 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)
+ continue;
}
- foreach (uint ownerId in prepared.RetainedOwnerIds)
+ RuntimeCollisionGenerationCommit result =
+ physics.CommitCollisionGeneration(admission, prepared);
+ if (result.Completed)
+ return result;
+ while (physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot projection))
{
- physics.RefreshCollisionRetainedOwner(
- admission,
- prepared,
- ownerId);
+ Assert.True(physics.SetPosition.AcknowledgeProjection(
+ projection.Token));
}
- RuntimeCollisionSealStep seal;
- do
- {
- seal = physics.AdvanceCollisionGenerationSeal(
- admission,
- prepared);
- }
- while (!seal.Completed && !seal.Restarted);
- if (seal.Completed)
- break;
+ engineCommitted = result.EngineCommitted;
}
- return physics.CommitCollisionGeneration(admission, prepared);
+ throw new InvalidOperationException(
+ "Collision generation did not complete its Runtime mutation transaction.");
}
private static void AddSyntheticCell(