feat(runtime): atomically replace collision generations
This commit is contained in:
parent
99bf1751bb
commit
9b0f59bd1b
21 changed files with 2435 additions and 408 deletions
|
|
@ -70,15 +70,46 @@ public sealed class LandblockPhysicsPublication : IDisposable
|
||||||
internal int RefloodCursor { get; set; }
|
internal int RefloodCursor { get; set; }
|
||||||
internal bool RefloodCommitted { get; set; }
|
internal bool RefloodCommitted { get; set; }
|
||||||
internal bool SealCommitted { get; set; }
|
internal bool SealCommitted { get; set; }
|
||||||
|
internal bool EngineMutationCommitted { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The last Runtime mutation poll was nonterminal. This includes exact
|
||||||
|
/// placement acknowledgements, collision-report/shadow dispatch debt, and
|
||||||
|
/// Runtime's deliberate first quiescence poll.
|
||||||
|
/// </summary>
|
||||||
|
internal bool RuntimeMutationPending { get; set; }
|
||||||
internal bool BeginCommitted { get; set; }
|
internal bool BeginCommitted { get; set; }
|
||||||
internal bool CompletionCommitted { get; set; }
|
internal bool CompletionCommitted { get; set; }
|
||||||
|
internal bool CancellationRequested { get; private set; }
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!CompletionCommitted)
|
if (!TryCancel())
|
||||||
Physics.CancelCollisionGeneration(
|
{
|
||||||
CollisionAdmission,
|
throw new InvalidOperationException(
|
||||||
PreparedGeneration);
|
"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;
|
public uint LandblockId => Build.Landblock.LandblockId;
|
||||||
|
|
@ -246,9 +277,14 @@ public sealed class LandblockPhysicsPublisher
|
||||||
publication.SetupObjectIds);
|
publication.SetupObjectIds);
|
||||||
return publication;
|
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;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -416,7 +452,7 @@ public sealed class LandblockPhysicsPublisher
|
||||||
/// presentation pipeline supplies the static-presentation owner callback
|
/// presentation pipeline supplies the static-presentation owner callback
|
||||||
/// that preserves per-entity light-before-collision order.
|
/// that preserves per-entity light-before-collision order.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void CompletePublication(
|
public bool CompletePublication(
|
||||||
LandblockPhysicsPublication publication,
|
LandblockPhysicsPublication publication,
|
||||||
Action<WorldEntity>? beforeStaticCollision = null)
|
Action<WorldEntity>? beforeStaticCollision = null)
|
||||||
{
|
{
|
||||||
|
|
@ -426,7 +462,29 @@ public sealed class LandblockPhysicsPublisher
|
||||||
"Physics publication cannot complete before its prefix commits.");
|
"Physics publication cannot complete before its prefix commits.");
|
||||||
while (!AdvanceCompleteOne(publication, beforeStaticCollision))
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -438,11 +496,15 @@ public sealed class LandblockPhysicsPublisher
|
||||||
Action<WorldEntity>? beforeStaticCollision = null)
|
Action<WorldEntity>? beforeStaticCollision = null)
|
||||||
{
|
{
|
||||||
ValidateReceipt(publication);
|
ValidateReceipt(publication);
|
||||||
|
if (publication.CancellationRequested)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"A cancelled collision publication cannot resume.");
|
||||||
if (!publication.BeginCommitted)
|
if (!publication.BeginCommitted)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Physics publication cannot complete before its prefix commits.");
|
"Physics publication cannot complete before its prefix commits.");
|
||||||
if (publication.CompletionCommitted)
|
if (publication.CompletionCommitted)
|
||||||
return true;
|
return true;
|
||||||
|
publication.RuntimeMutationPending = false;
|
||||||
|
|
||||||
long started = Stopwatch.GetTimestamp();
|
long started = Stopwatch.GetTimestamp();
|
||||||
LoadedLandblock landblock = publication.Build.Landblock;
|
LoadedLandblock landblock = publication.Build.Landblock;
|
||||||
|
|
@ -567,12 +629,20 @@ public sealed class LandblockPhysicsPublisher
|
||||||
_physics.CommitCollisionGeneration(
|
_physics.CommitCollisionGeneration(
|
||||||
publication.CollisionAdmission,
|
publication.CollisionAdmission,
|
||||||
publication.PreparedGeneration);
|
publication.PreparedGeneration);
|
||||||
if (!commit.Committed)
|
publication.EngineMutationCommitted |= commit.EngineCommitted;
|
||||||
|
if (!commit.Completed)
|
||||||
{
|
{
|
||||||
// Runtime coalesces post-seal arrivals in its owner journal.
|
publication.RuntimeMutationPending = true;
|
||||||
// Resume that seal tail rather than restarting the
|
if (!commit.EngineCommitted)
|
||||||
// completed generation-wide capture/reflood pass.
|
{
|
||||||
publication.SealCommitted = false;
|
// 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;
|
_completePublishTicks += Stopwatch.GetTimestamp() - started;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -587,16 +657,54 @@ public sealed class LandblockPhysicsPublisher
|
||||||
return publication.CompletionCommitted;
|
return publication.CompletionCommitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DemoteToTerrain(uint landblockId)
|
public bool DemoteToTerrain(uint landblockId)
|
||||||
{
|
{
|
||||||
_physics.DemoteCollisionToTerrain(landblockId);
|
for (int poll = 0; poll < 2; poll++)
|
||||||
_demotionCount++;
|
{
|
||||||
|
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++;
|
_fullRemovalCount++;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PublishCell(
|
private void PublishCell(
|
||||||
|
|
@ -1066,7 +1174,8 @@ public sealed class LandblockPhysicsPublisher
|
||||||
"The physics publication receipt belongs to another publisher.",
|
"The physics publication receipt belongs to another publisher.",
|
||||||
nameof(publication));
|
nameof(publication));
|
||||||
}
|
}
|
||||||
if (!publication.CompletionCommitted)
|
if (!publication.CompletionCommitted
|
||||||
|
&& !publication.EngineMutationCommitted)
|
||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(
|
ObjectDisposedException.ThrowIf(
|
||||||
publication.PreparedGeneration.IsDisposed,
|
publication.PreparedGeneration.IsDisposed,
|
||||||
|
|
|
||||||
|
|
@ -217,14 +217,34 @@ public sealed class LandblockPresentationPipeline
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cancels retained publication receipts during a generation reset. A
|
/// Cancels retained publication receipts during a generation reset. A
|
||||||
/// collision receipt owns only its private staging world until activation,
|
/// pre-engine receipt restores its exact prior generation. A receipt that
|
||||||
/// so cancellation cannot withdraw or partially replace the active world.
|
/// already transferred the engine generation remains retained until its
|
||||||
|
/// canonical post-engine placement acknowledgement suffix completes; the
|
||||||
|
/// committed transfer is never rolled back.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void CancelPendingPublications()
|
internal bool CancelPendingPublications()
|
||||||
{
|
{
|
||||||
foreach (PublicationTransaction transaction in _publications.Values)
|
if (_publications.Count == 0)
|
||||||
transaction.PhysicsPublication?.Dispose();
|
return true;
|
||||||
_publications.Clear();
|
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)
|
public void ResumePublication(LandblockStreamResult result)
|
||||||
|
|
@ -247,10 +267,15 @@ public sealed class LandblockPresentationPipeline
|
||||||
return Advance(result, transaction, meter, ensureProgress);
|
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);
|
_retirements.Advance(meter);
|
||||||
|
}
|
||||||
|
|
||||||
internal void AdvancePriorityRetirement(
|
internal void AdvancePriorityRetirement(
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
|
|
@ -261,14 +286,26 @@ public sealed class LandblockPresentationPipeline
|
||||||
{
|
{
|
||||||
_retirements.BeginFull(landblockId);
|
_retirements.BeginFull(landblockId);
|
||||||
if (_retirements.UsesBudgetedSteps)
|
if (_retirements.UsesBudgetedSteps)
|
||||||
|
{
|
||||||
_retirements.Advance();
|
_retirements.Advance();
|
||||||
|
if (_retirements.IsPending(landblockId)
|
||||||
|
&& (_physicsPublisher?.CanContinueMutationSynchronously()
|
||||||
|
?? true))
|
||||||
|
_retirements.Advance();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BeginNearLayerRetirement(uint landblockId)
|
public void BeginNearLayerRetirement(uint landblockId)
|
||||||
{
|
{
|
||||||
_retirements.BeginNearLayer(landblockId);
|
_retirements.BeginNearLayer(landblockId);
|
||||||
if (_retirements.UsesBudgetedSteps)
|
if (_retirements.UsesBudgetedSteps)
|
||||||
|
{
|
||||||
_retirements.Advance();
|
_retirements.Advance();
|
||||||
|
if (_retirements.IsPending(landblockId)
|
||||||
|
&& (_physicsPublisher?.CanContinueMutationSynchronously()
|
||||||
|
?? true))
|
||||||
|
_retirements.Advance();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void EnqueueFullRetirement(uint landblockId) =>
|
internal void EnqueueFullRetirement(uint landblockId) =>
|
||||||
|
|
@ -754,6 +791,17 @@ public sealed class LandblockPresentationPipeline
|
||||||
{
|
{
|
||||||
return new LandblockPublicationAdvance(false, progressed);
|
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)
|
while (!transaction.StaticPublication.CompletionCommitted)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -73,22 +73,20 @@ public sealed class LandblockPresentationRetirementOwner
|
||||||
static entity => entity.ServerGuid == 0,
|
static entity => entity.ServerGuid == 0,
|
||||||
_staticPresentation.RemovePluginProjection);
|
_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)
|
if (ticket.Kind == LandblockRetirementKind.Full)
|
||||||
{
|
{
|
||||||
ticket.RunOnce(
|
ticket.RunOnce(
|
||||||
LandblockRetirementStage.Terrain,
|
LandblockRetirementStage.Terrain,
|
||||||
() => _render.RemoveTerrain(ticket.LandblockId));
|
() => _render.RemoveTerrain(ticket.LandblockId));
|
||||||
}
|
}
|
||||||
|
|
||||||
ticket.RunOnce(
|
|
||||||
LandblockRetirementStage.Physics,
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
if (ticket.Kind == LandblockRetirementKind.Full)
|
|
||||||
_physics.RemoveLandblock(ticket.LandblockId);
|
|
||||||
else
|
|
||||||
_physics.DemoteToTerrain(ticket.LandblockId);
|
|
||||||
});
|
|
||||||
ticket.RunOnce(
|
ticket.RunOnce(
|
||||||
LandblockRetirementStage.CellVisibility,
|
LandblockRetirementStage.CellVisibility,
|
||||||
() => _render.RemoveCellVisibility(ticket.LandblockId));
|
() => _render.RemoveCellVisibility(ticket.LandblockId));
|
||||||
|
|
@ -128,13 +126,9 @@ public sealed class LandblockPresentationRetirementOwner
|
||||||
LandblockRetirementStage.Physics =>
|
LandblockRetirementStage.Physics =>
|
||||||
ticket.RunOnceStep(
|
ticket.RunOnceStep(
|
||||||
LandblockRetirementStage.Physics,
|
LandblockRetirementStage.Physics,
|
||||||
() =>
|
() => ticket.Kind == LandblockRetirementKind.Full
|
||||||
{
|
? _physics.AdvanceRemoval(ticket.LandblockId)
|
||||||
if (ticket.Kind == LandblockRetirementKind.Full)
|
: _physics.AdvanceDemotion(ticket.LandblockId)),
|
||||||
_physics.RemoveLandblock(ticket.LandblockId);
|
|
||||||
else
|
|
||||||
_physics.DemoteToTerrain(ticket.LandblockId);
|
|
||||||
}),
|
|
||||||
LandblockRetirementStage.CellVisibility =>
|
LandblockRetirementStage.CellVisibility =>
|
||||||
ticket.RunOnceStep(
|
ticket.RunOnceStep(
|
||||||
LandblockRetirementStage.CellVisibility,
|
LandblockRetirementStage.CellVisibility,
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ public enum LandblockRetirementStage : ushort
|
||||||
EntityLighting = 1 << 3,
|
EntityLighting = 1 << 3,
|
||||||
EntityTranslucency = 1 << 4,
|
EntityTranslucency = 1 << 4,
|
||||||
PluginProjection = 1 << 5,
|
PluginProjection = 1 << 5,
|
||||||
Terrain = 1 << 6,
|
Physics = 1 << 6,
|
||||||
Physics = 1 << 7,
|
Terrain = 1 << 7,
|
||||||
CellVisibility = 1 << 8,
|
CellVisibility = 1 << 8,
|
||||||
BuildingRegistry = 1 << 9,
|
BuildingRegistry = 1 << 9,
|
||||||
EnvironmentCells = 1 << 10,
|
EnvironmentCells = 1 << 10,
|
||||||
|
|
@ -24,6 +24,7 @@ internal enum LandblockRetirementOperationResult : byte
|
||||||
{
|
{
|
||||||
NoWork,
|
NoWork,
|
||||||
Progressed,
|
Progressed,
|
||||||
|
Pending,
|
||||||
Failed,
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,6 +78,28 @@ public sealed class LandblockRetirementTicket
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool RunOnce(LandblockRetirementStage stage, Func<bool> 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(
|
internal LandblockRetirementOperationResult RunOnceStep(
|
||||||
LandblockRetirementStage stage,
|
LandblockRetirementStage stage,
|
||||||
Action operation)
|
Action operation)
|
||||||
|
|
@ -100,6 +123,31 @@ public sealed class LandblockRetirementTicket
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal LandblockRetirementOperationResult RunOnceStep(
|
||||||
|
LandblockRetirementStage stage,
|
||||||
|
Func<bool> 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(
|
public bool RunForEachEntity(
|
||||||
LandblockRetirementStage stage,
|
LandblockRetirementStage stage,
|
||||||
Func<WorldEntity, bool> predicate,
|
Func<WorldEntity, bool> predicate,
|
||||||
|
|
@ -815,6 +863,8 @@ public sealed class LandblockRetirementCoordinator
|
||||||
}
|
}
|
||||||
|
|
||||||
meter.Complete();
|
meter.Complete();
|
||||||
|
if (result == LandblockRetirementOperationResult.Pending)
|
||||||
|
return BudgetedAdvanceResult.Yielded;
|
||||||
return BudgetedAdvanceResult.Progressed;
|
return BudgetedAdvanceResult.Progressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1362,19 +1362,23 @@ public sealed class StreamingController
|
||||||
{
|
{
|
||||||
if (!transaction.PendingPublicationsCleared)
|
if (!transaction.PendingPublicationsCleared)
|
||||||
{
|
{
|
||||||
|
bool cancelled = false;
|
||||||
if (!TryRunStreamingWork(
|
if (!TryRunStreamingWork(
|
||||||
meter,
|
meter,
|
||||||
new StreamingWorkCost(EntityOperations: 1),
|
new StreamingWorkCost(EntityOperations: 1),
|
||||||
"recenter-cancel-publications",
|
"recenter-cancel-publications",
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
_presentation.CancelPendingPublications();
|
cancelled =
|
||||||
transaction.PendingPublicationsCleared = true;
|
_presentation.CancelPendingPublications();
|
||||||
return true;
|
return true;
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
transaction.PendingPublicationsCleared = cancelled;
|
||||||
|
if (!cancelled)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (!TryRunStreamingWork(
|
if (!TryRunStreamingWork(
|
||||||
meter,
|
meter,
|
||||||
|
|
@ -1519,19 +1523,23 @@ public sealed class StreamingController
|
||||||
{
|
{
|
||||||
if (!transaction.PendingPublicationsCleared)
|
if (!transaction.PendingPublicationsCleared)
|
||||||
{
|
{
|
||||||
|
bool cancelled = false;
|
||||||
if (!TryRunStreamingWork(
|
if (!TryRunStreamingWork(
|
||||||
meter,
|
meter,
|
||||||
new StreamingWorkCost(EntityOperations: 1),
|
new StreamingWorkCost(EntityOperations: 1),
|
||||||
"reload-cancel-publications",
|
"reload-cancel-publications",
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
_presentation.CancelPendingPublications();
|
cancelled =
|
||||||
transaction.PendingPublicationsCleared = true;
|
_presentation.CancelPendingPublications();
|
||||||
return true;
|
return true;
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
transaction.PendingPublicationsCleared = cancelled;
|
||||||
|
if (!cancelled)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (!TryRunStreamingWork(
|
if (!TryRunStreamingWork(
|
||||||
meter,
|
meter,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@
|
||||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||||
<_Parameter1>AcDream.Runtime.Tests</_Parameter1>
|
<_Parameter1>AcDream.Runtime.Tests</_Parameter1>
|
||||||
</AssemblyAttribute>
|
</AssemblyAttribute>
|
||||||
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||||
|
<_Parameter1>AcDream.App.Tests</_Parameter1>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||||
|
<_Parameter1>AcDream.Headless.Tests</_Parameter1>
|
||||||
|
</AssemblyAttribute>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" />
|
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -1106,7 +1106,7 @@ public sealed class PhysicsEngine
|
||||||
/// Register a landblock with its terrain surface, indoor cells, portal
|
/// Register a landblock with its terrain surface, indoor cells, portal
|
||||||
/// planes, and world-space origin offset.
|
/// planes, and world-space origin offset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void AddLandblock(uint landblockId, TerrainSurface terrain,
|
internal void AddLandblock(uint landblockId, TerrainSurface terrain,
|
||||||
IReadOnlyList<CellSurface> cells, IReadOnlyList<PortalPlane> portals,
|
IReadOnlyList<CellSurface> cells, IReadOnlyList<PortalPlane> portals,
|
||||||
float worldOffsetX, float worldOffsetY)
|
float worldOffsetX, float worldOffsetY)
|
||||||
{
|
{
|
||||||
|
|
@ -1120,7 +1120,7 @@ public sealed class PhysicsEngine
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Remove a previously registered landblock, including its shadow objects.
|
/// Remove a previously registered landblock, including its shadow objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RemoveLandblock(uint landblockId)
|
internal void RemoveLandblock(uint landblockId)
|
||||||
{
|
{
|
||||||
_landblocks.Remove(landblockId);
|
_landblocks.Remove(landblockId);
|
||||||
RemoveLandblockSlot(landblockId);
|
RemoveLandblockSlot(landblockId);
|
||||||
|
|
@ -1152,7 +1152,7 @@ public sealed class PhysicsEngine
|
||||||
/// owned by this engine. Runtime calls this only at terminal disposal;
|
/// owned by this engine. Runtime calls this only at terminal disposal;
|
||||||
/// ordinary streaming still uses the typed per-landblock retirement path.
|
/// ordinary streaming still uses the typed per-landblock retirement path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Clear()
|
internal void Clear()
|
||||||
{
|
{
|
||||||
if (_landblocks.Count != 0)
|
if (_landblocks.Count != 0)
|
||||||
{
|
{
|
||||||
|
|
@ -1173,7 +1173,7 @@ public sealed class PhysicsEngine
|
||||||
/// while preserving its terrain surface and world offset for Far-tier use.
|
/// while preserving its terrain surface and world offset for Far-tier use.
|
||||||
/// The corresponding render-side demotion preserves the terrain slot too.
|
/// The corresponding render-side demotion preserves the terrain slot too.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void DemoteLandblockToTerrain(uint landblockId)
|
internal void DemoteLandblockToTerrain(uint landblockId)
|
||||||
{
|
{
|
||||||
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||||
if (_landblocks.TryGetValue(canonical, out var landblock))
|
if (_landblocks.TryGetValue(canonical, out var landblock))
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,37 @@ internal interface IHeadlessCollisionNeighborhood
|
||||||
bool IsReady(uint fullCellId);
|
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,
|
RuntimePhysicsState physics,
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
Action<RuntimeCollisionAdmission>? afterAdmission,
|
Action<RuntimeCollisionAdmission>? afterAdmission,
|
||||||
|
|
@ -33,60 +61,103 @@ internal static class HeadlessCollisionGenerationTransaction
|
||||||
RuntimeCollisionAdmission admission =
|
RuntimeCollisionAdmission admission =
|
||||||
physics.BeginCollisionAdmission(landblockId);
|
physics.BeginCollisionAdmission(landblockId);
|
||||||
PreparedLandblockCollisionGeneration? prepared = null;
|
PreparedLandblockCollisionGeneration? prepared = null;
|
||||||
bool committed = false;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// This hook exists so the exact post-admission/pre-prepare failure
|
|
||||||
// boundary remains covered. Production does not install one.
|
|
||||||
afterAdmission?.Invoke(admission);
|
afterAdmission?.Invoke(admission);
|
||||||
prepared = physics.PrepareCollisionGeneration(admission);
|
prepared = physics.PrepareCollisionGeneration(admission);
|
||||||
stage(admission, prepared);
|
stage(admission, prepared);
|
||||||
|
return new HeadlessCollisionGenerationTransaction(
|
||||||
RuntimeCollisionOwnerCaptureStep ownerCapture;
|
physics,
|
||||||
do
|
admission,
|
||||||
{
|
prepared);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
finally
|
catch (Exception publicationError)
|
||||||
{
|
{
|
||||||
if (!committed)
|
if (!physics.CancelCollisionGeneration(admission, prepared))
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -97,10 +168,23 @@ internal static class HeadlessCollisionGenerationTransaction
|
||||||
internal sealed class HeadlessCollisionNeighborhood
|
internal sealed class HeadlessCollisionNeighborhood
|
||||||
: IHeadlessCollisionNeighborhood
|
: IHeadlessCollisionNeighborhood
|
||||||
{
|
{
|
||||||
|
private readonly record struct PublicationSpec(
|
||||||
|
uint LandblockId,
|
||||||
|
Vector3 Origin,
|
||||||
|
bool Required);
|
||||||
|
|
||||||
private readonly GameRuntime _runtime;
|
private readonly GameRuntime _runtime;
|
||||||
private readonly HeadlessProcessContentOwner
|
private readonly HeadlessProcessContentOwner
|
||||||
.HeadlessProcessContentLease _content;
|
.HeadlessProcessContentLease _content;
|
||||||
private readonly HashSet<uint> _resident = [];
|
private readonly HashSet<uint> _resident = [];
|
||||||
|
private readonly Queue<uint> _retirementQueue = [];
|
||||||
|
private readonly Queue<PublicationSpec> _publicationQueue = [];
|
||||||
|
private HeadlessCollisionGenerationTransaction? _pendingPublication;
|
||||||
|
private bool _pendingPublicationCancellation;
|
||||||
|
private bool _resetRequired;
|
||||||
|
private bool _publicationPlanBuilt;
|
||||||
|
private uint _requestedCenterLandblock;
|
||||||
|
private uint _requestedFullCell;
|
||||||
private uint _centerLandblock;
|
private uint _centerLandblock;
|
||||||
|
|
||||||
internal HeadlessCollisionNeighborhood(
|
internal HeadlessCollisionNeighborhood(
|
||||||
|
|
@ -122,6 +206,21 @@ internal sealed class HeadlessCollisionNeighborhood
|
||||||
nameof(fullCellId),
|
nameof(fullCellId),
|
||||||
"A collision neighborhood requires a real destination cell.");
|
"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
|
if (_centerLandblock == center
|
||||||
&& _resident.Contains(center)
|
&& _resident.Contains(center)
|
||||||
&& _runtime.EntityObjects.Physics.Engine
|
&& _runtime.EntityObjects.Physics.Engine
|
||||||
|
|
@ -131,55 +230,17 @@ internal sealed class HeadlessCollisionNeighborhood
|
||||||
.UpdatePlayerCurrCell(fullCellId);
|
.UpdatePlayerCurrCell(fullCellId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
AdvanceWork();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsReady(uint fullCellId)
|
public bool IsReady(uint fullCellId)
|
||||||
{
|
{
|
||||||
uint center = CanonicalLandblock(fullCellId);
|
uint center = CanonicalLandblock(fullCellId);
|
||||||
|
if (_requestedCenterLandblock == center)
|
||||||
|
{
|
||||||
|
_requestedFullCell = fullCellId;
|
||||||
|
AdvanceWork();
|
||||||
|
}
|
||||||
if (_centerLandblock != center
|
if (_centerLandblock != center
|
||||||
|| !_resident.Contains(center)
|
|| !_resident.Contains(center)
|
||||||
|| !_runtime.EntityObjects.Physics.Engine
|
|| !_runtime.EntityObjects.Physics.Engine
|
||||||
|
|
@ -192,7 +253,7 @@ internal sealed class HeadlessCollisionNeighborhood
|
||||||
.GetCellStruct(fullCellId) is not null;
|
.GetCellStruct(fullCellId) is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PublishOne(
|
private HeadlessCollisionGenerationTransaction? CreatePublication(
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
Vector3 origin,
|
Vector3 origin,
|
||||||
uint currentCellId,
|
uint currentCellId,
|
||||||
|
|
@ -207,7 +268,7 @@ internal sealed class HeadlessCollisionNeighborhood
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"Required headless landblock 0x{landblockId:X8} is missing.");
|
$"Required headless landblock 0x{landblockId:X8} is missing.");
|
||||||
}
|
}
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
IReadOnlyList<WorldEntity> staticEntities =
|
IReadOnlyList<WorldEntity> staticEntities =
|
||||||
|
|
@ -244,7 +305,7 @@ internal sealed class HeadlessCollisionNeighborhood
|
||||||
landblock);
|
landblock);
|
||||||
|
|
||||||
RuntimePhysicsState physics = _runtime.EntityObjects.Physics;
|
RuntimePhysicsState physics = _runtime.EntityObjects.Physics;
|
||||||
_ = HeadlessCollisionGenerationTransaction.Execute(
|
return HeadlessCollisionGenerationTransaction.Begin(
|
||||||
physics,
|
physics,
|
||||||
landblockId,
|
landblockId,
|
||||||
afterAdmission: null,
|
afterAdmission: null,
|
||||||
|
|
@ -294,22 +355,113 @@ internal sealed class HeadlessCollisionNeighborhood
|
||||||
collisions,
|
collisions,
|
||||||
origin);
|
origin);
|
||||||
});
|
});
|
||||||
_resident.Add(CanonicalLandblock(landblockId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RetireAll()
|
private void AdvanceWork()
|
||||||
{
|
{
|
||||||
if (_resident.Count == 0)
|
if (_pendingPublicationCancellation
|
||||||
|
&& _pendingPublication is { } cancelling)
|
||||||
{
|
{
|
||||||
_centerLandblock = 0u;
|
bool wasCommitted = cancelling.EngineMutationCommitted;
|
||||||
return;
|
if (!cancelling.TryCancel())
|
||||||
|
return;
|
||||||
|
if (wasCommitted || cancelling.EngineMutationCommitted)
|
||||||
|
_resident.Add(cancelling.LandblockId);
|
||||||
|
_pendingPublication = null;
|
||||||
|
_pendingPublicationCancellation = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint[] retiring = [.. _resident];
|
if (_resetRequired)
|
||||||
_resident.Clear();
|
{
|
||||||
_centerLandblock = 0u;
|
_retirementQueue.Clear();
|
||||||
foreach (uint landblock in retiring)
|
foreach (uint landblock in _resident)
|
||||||
_ = _runtime.EntityObjects.Physics.WithdrawCollision(landblock);
|
_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<PublicationSpec> 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) =>
|
private static uint CanonicalLandblock(uint fullCellId) =>
|
||||||
|
|
|
||||||
|
|
@ -1053,8 +1053,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
_sessionClearInProgress = true;
|
_sessionClearInProgress = true;
|
||||||
RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray();
|
RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray();
|
||||||
Physics.CollisionReports.LeaveWorldBatch(active);
|
Physics.CollisionReports.LeaveWorldBatch(active);
|
||||||
Physics.SetPosition.ResetSession();
|
Physics.ResetSessionPhysics();
|
||||||
Physics.CollisionReports.ResetSession();
|
|
||||||
Entities.BeginSessionClear();
|
Entities.BeginSessionClear();
|
||||||
foreach (RuntimeEntityRecord canonical in active)
|
foreach (RuntimeEntityRecord canonical in active)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -388,6 +388,14 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
return RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority;
|
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,
|
// All validation is complete. The remaining stores are callback-free,
|
||||||
// non-allocating, and cannot fail on this single Runtime update thread.
|
// non-allocating, and cannot fail on this single Runtime update thread.
|
||||||
// The controller remains RuntimeOwnedDormant; the subsequent world
|
// The controller remains RuntimeOwnedDormant; the subsequent world
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
|
||||||
bool IsCollisionReportDispatching,
|
bool IsCollisionReportDispatching,
|
||||||
int CollisionPrefixQuiescenceCount,
|
int CollisionPrefixQuiescenceCount,
|
||||||
int PendingCollisionPrefixProjectionCount,
|
int PendingCollisionPrefixProjectionCount,
|
||||||
|
int CollisionPrefixMutationCount,
|
||||||
|
int CommittedCollisionPrefixMutationCount,
|
||||||
int CollisionAdmissionCount,
|
int CollisionAdmissionCount,
|
||||||
int CollisionGenerationCount,
|
int CollisionGenerationCount,
|
||||||
bool OwnsProductionDataCache,
|
bool OwnsProductionDataCache,
|
||||||
|
|
@ -75,6 +77,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
|
||||||
&& !IsCollisionReportDispatching
|
&& !IsCollisionReportDispatching
|
||||||
&& CollisionPrefixQuiescenceCount == 0
|
&& CollisionPrefixQuiescenceCount == 0
|
||||||
&& PendingCollisionPrefixProjectionCount == 0
|
&& PendingCollisionPrefixProjectionCount == 0
|
||||||
|
&& CollisionPrefixMutationCount == 0
|
||||||
|
&& CommittedCollisionPrefixMutationCount == 0
|
||||||
&& CollisionAdmissionCount == 0
|
&& CollisionAdmissionCount == 0
|
||||||
&& CollisionGenerationCount == 0
|
&& CollisionGenerationCount == 0
|
||||||
&& OwnsProductionDataCache;
|
&& OwnsProductionDataCache;
|
||||||
|
|
@ -100,11 +104,13 @@ public sealed class RuntimeCollisionAdmission
|
||||||
internal RuntimeCollisionAdmission(
|
internal RuntimeCollisionAdmission(
|
||||||
RuntimePhysicsState owner,
|
RuntimePhysicsState owner,
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
ulong generation)
|
ulong generation,
|
||||||
|
ulong previousGeneration)
|
||||||
{
|
{
|
||||||
Owner = owner;
|
Owner = owner;
|
||||||
LandblockId = landblockId;
|
LandblockId = landblockId;
|
||||||
Generation = generation;
|
Generation = generation;
|
||||||
|
PreviousGeneration = previousGeneration;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal RuntimePhysicsState Owner { get; }
|
internal RuntimePhysicsState Owner { get; }
|
||||||
|
|
@ -112,6 +118,32 @@ public sealed class RuntimeCollisionAdmission
|
||||||
internal bool Completed { get; set; }
|
internal bool Completed { get; set; }
|
||||||
public uint LandblockId { get; }
|
public uint LandblockId { get; }
|
||||||
public ulong Generation { 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(
|
public readonly record struct RuntimeCollisionAcknowledgement(
|
||||||
|
|
@ -120,11 +152,23 @@ public readonly record struct RuntimeCollisionAcknowledgement(
|
||||||
bool WasResident,
|
bool WasResident,
|
||||||
bool Ready);
|
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(
|
public readonly record struct RuntimeCollisionGenerationCommit(
|
||||||
RuntimeCollisionAcknowledgement Acknowledgement,
|
RuntimeCollisionAcknowledgement Acknowledgement,
|
||||||
uint[] DirtyRetainedOwnerIds)
|
uint[] DirtyRetainedOwnerIds,
|
||||||
|
bool EngineCommitted,
|
||||||
|
bool Completed)
|
||||||
{
|
{
|
||||||
public bool Committed => Acknowledgement.Ready;
|
public bool Committed => Completed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly record struct RuntimeCollisionGenerationCommitted(
|
public readonly record struct RuntimeCollisionGenerationCommitted(
|
||||||
|
|
@ -1045,6 +1089,8 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
_collisionAdmissions = new();
|
_collisionAdmissions = new();
|
||||||
private readonly Dictionary<uint, PreparedLandblockCollisionGeneration>
|
private readonly Dictionary<uint, PreparedLandblockCollisionGeneration>
|
||||||
_preparedCollisionGenerations = new();
|
_preparedCollisionGenerations = new();
|
||||||
|
private readonly Dictionary<uint, RuntimeCollisionPrefixMutation>
|
||||||
|
_collisionPrefixMutations = new();
|
||||||
private readonly CollisionOwnerMutationJournal _collisionOwnerJournal = new();
|
private readonly CollisionOwnerMutationJournal _collisionOwnerJournal = new();
|
||||||
private readonly Dictionary<uint, List<PreparedLandblockCollisionGeneration>>
|
private readonly Dictionary<uint, List<PreparedLandblockCollisionGeneration>>
|
||||||
_collisionOwnerSubscribers = new();
|
_collisionOwnerSubscribers = new();
|
||||||
|
|
@ -1175,6 +1221,9 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
collisionReports.IsDispatching,
|
collisionReports.IsDispatching,
|
||||||
setPosition.CollisionPrefixQuiescenceCount,
|
setPosition.CollisionPrefixQuiescenceCount,
|
||||||
setPosition.PendingQuiescenceProjectionCount,
|
setPosition.PendingQuiescenceProjectionCount,
|
||||||
|
_collisionPrefixMutations.Count,
|
||||||
|
_collisionPrefixMutations.Values.Count(
|
||||||
|
mutation => mutation.EngineMutationCommitted),
|
||||||
_collisionAdmissions.Count,
|
_collisionAdmissions.Count,
|
||||||
_collisionGenerations.Count,
|
_collisionGenerations.Count,
|
||||||
ReferenceEquals(Engine.DataCache, DataCache),
|
ReferenceEquals(Engine.DataCache, DataCache),
|
||||||
|
|
@ -1907,6 +1956,29 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
_spatialRoots.Clear();
|
_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
|
internal RuntimeCollisionPrefixQuiescenceToken
|
||||||
BeginCollisionPrefixQuiescence(
|
BeginCollisionPrefixQuiescence(
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
|
|
@ -1957,25 +2029,29 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
successorReady);
|
successorReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool CompleteCollisionPrefixQuiescence(
|
|
||||||
in RuntimeCollisionPrefixMutationPermission permission)
|
|
||||||
{
|
|
||||||
EnsureNotDisposed();
|
|
||||||
EnsureCollisionMutationThread();
|
|
||||||
return SetPosition.CompleteCollisionPrefixQuiescence(permission);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RuntimeCollisionAdmission BeginCollisionAdmission(
|
public RuntimeCollisionAdmission BeginCollisionAdmission(
|
||||||
uint landblockId)
|
uint landblockId)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
EnsureCollisionMutationThread();
|
EnsureCollisionMutationThread();
|
||||||
uint canonical = CanonicalLandblock(landblockId);
|
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();
|
AdvanceCollisionWorldAuthority();
|
||||||
if (_collisionAdmissions.Remove(
|
if (_collisionAdmissions.Remove(
|
||||||
canonical,
|
canonical,
|
||||||
out RuntimeCollisionAdmission? superseded))
|
out RuntimeCollisionAdmission? superseded))
|
||||||
{
|
{
|
||||||
|
previousGeneration = superseded.PreviousGeneration;
|
||||||
SetPosition.CancelCollisionGeneration(
|
SetPosition.CancelCollisionGeneration(
|
||||||
canonical,
|
canonical,
|
||||||
superseded.Generation);
|
superseded.Generation);
|
||||||
|
|
@ -1986,16 +2062,13 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
prepared.Dispose();
|
prepared.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ulong generation = _collisionGenerations.TryGetValue(
|
ulong generation = checked(currentGeneration + 1UL);
|
||||||
canonical,
|
|
||||||
out ulong current)
|
|
||||||
? checked(current + 1UL)
|
|
||||||
: 1UL;
|
|
||||||
_collisionGenerations[canonical] = generation;
|
_collisionGenerations[canonical] = generation;
|
||||||
var admission = new RuntimeCollisionAdmission(
|
var admission = new RuntimeCollisionAdmission(
|
||||||
this,
|
this,
|
||||||
canonical,
|
canonical,
|
||||||
generation);
|
generation,
|
||||||
|
previousGeneration);
|
||||||
_collisionAdmissions[canonical] = admission;
|
_collisionAdmissions[canonical] = admission;
|
||||||
SetPosition.BeginCollisionGeneration(canonical, generation);
|
SetPosition.BeginCollisionGeneration(canonical, generation);
|
||||||
return admission;
|
return admission;
|
||||||
|
|
@ -2047,7 +2120,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
/// collision world is never withdrawn. A stale receipt may dispose its
|
/// collision world is never withdrawn. A stale receipt may dispose its
|
||||||
/// own staging storage but cannot invalidate a newer admission.
|
/// own staging storage but cannot invalidate a newer admission.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void CancelCollisionGeneration(
|
internal bool CancelCollisionGeneration(
|
||||||
RuntimeCollisionAdmission admission,
|
RuntimeCollisionAdmission admission,
|
||||||
PreparedLandblockCollisionGeneration? prepared = null)
|
PreparedLandblockCollisionGeneration? prepared = null)
|
||||||
{
|
{
|
||||||
|
|
@ -2067,6 +2140,38 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
nameof(prepared));
|
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();
|
prepared?.Dispose();
|
||||||
if (prepared is not null
|
if (prepared is not null
|
||||||
&& _preparedCollisionGenerations.TryGetValue(
|
&& _preparedCollisionGenerations.TryGetValue(
|
||||||
|
|
@ -2090,6 +2195,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
AdvanceCollisionWorldAuthority();
|
AdvanceCollisionWorldAuthority();
|
||||||
}
|
}
|
||||||
TrimCollisionOwnerJournal();
|
TrimCollisionOwnerJournal();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void StageCollisionAssets(
|
internal void StageCollisionAssets(
|
||||||
|
|
@ -2261,8 +2367,29 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
RuntimeCollisionAdmission admission,
|
RuntimeCollisionAdmission admission,
|
||||||
PreparedLandblockCollisionGeneration prepared)
|
PreparedLandblockCollisionGeneration prepared)
|
||||||
{
|
{
|
||||||
ValidateAdmission(admission);
|
EnsureNotDisposed();
|
||||||
EnsureCollisionMutationThread();
|
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);
|
ValidatePreparedGeneration(admission, prepared);
|
||||||
if (!admission.AssetsPrepared)
|
if (!admission.AssetsPrepared)
|
||||||
{
|
{
|
||||||
|
|
@ -2292,8 +2419,62 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
admission.Generation,
|
admission.Generation,
|
||||||
Engine.IsLandblockTerrainResident(admission.LandblockId),
|
Engine.IsLandblockTerrainResident(admission.LandblockId),
|
||||||
Ready: false),
|
Ready: false),
|
||||||
Array.Empty<uint>());
|
Array.Empty<uint>(),
|
||||||
|
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 =
|
PhysicsEngine.PreparedPhysicsEngineLandblock replacement =
|
||||||
prepared.TakeSealedReplacement();
|
prepared.TakeSealedReplacement();
|
||||||
bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
|
bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
|
||||||
|
|
@ -2313,20 +2494,70 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
if (later.Sequence > prepared.Sequence)
|
if (later.Sequence > prepared.Sequence)
|
||||||
later.EnqueueCommittedRebase(replacement);
|
later.EnqueueCommittedRebase(replacement);
|
||||||
}
|
}
|
||||||
admission.Completed = true;
|
|
||||||
_collisionAdmissions.Remove(admission.LandblockId);
|
|
||||||
_preparedCollisionGenerations.Remove(admission.LandblockId);
|
_preparedCollisionGenerations.Remove(admission.LandblockId);
|
||||||
prepared.MarkCommitted();
|
prepared.MarkCommitted();
|
||||||
TrimCollisionOwnerJournal();
|
mutation.EngineMutationCommitted = true;
|
||||||
var acknowledgement = new RuntimeCollisionAcknowledgement(
|
mutation.Ready = Engine.IsLandblockTerrainResident(
|
||||||
admission.LandblockId,
|
admission.LandblockId);
|
||||||
admission.Generation,
|
mutation.WasResident = mutation.Ready;
|
||||||
Engine.IsLandblockTerrainResident(admission.LandblockId),
|
|
||||||
Ready: Engine.IsLandblockTerrainResident(admission.LandblockId));
|
|
||||||
SetPosition.CommitCollisionGeneration(
|
SetPosition.CommitCollisionGeneration(
|
||||||
acknowledgement.LandblockId,
|
mutation.LandblockId,
|
||||||
acknowledgement.Generation,
|
mutation.TargetGeneration,
|
||||||
acknowledgement.Ready);
|
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<uint>(),
|
||||||
|
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<uint>(),
|
||||||
|
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(
|
PublishCollisionGenerationCommitted(
|
||||||
new RuntimeCollisionGenerationCommitted(
|
new RuntimeCollisionGenerationCommitted(
|
||||||
acknowledgement.LandblockId,
|
acknowledgement.LandblockId,
|
||||||
|
|
@ -2334,67 +2565,179 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
acknowledgement.Ready));
|
acknowledgement.Ready));
|
||||||
return new RuntimeCollisionGenerationCommit(
|
return new RuntimeCollisionGenerationCommit(
|
||||||
acknowledgement,
|
acknowledgement,
|
||||||
Array.Empty<uint>());
|
Array.Empty<uint>(),
|
||||||
|
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)
|
uint landblockId)
|
||||||
|
=> AdvanceCollisionRetirementMutation(
|
||||||
|
landblockId,
|
||||||
|
RuntimeCollisionPrefixMutationKind.Demotion);
|
||||||
|
|
||||||
|
public RuntimeCollisionMutationResult WithdrawCollision(
|
||||||
|
uint landblockId)
|
||||||
|
=> AdvanceCollisionRetirementMutation(
|
||||||
|
landblockId,
|
||||||
|
RuntimeCollisionPrefixMutationKind.Withdrawal);
|
||||||
|
|
||||||
|
private RuntimeCollisionMutationResult AdvanceCollisionRetirementMutation(
|
||||||
|
uint landblockId,
|
||||||
|
RuntimeCollisionPrefixMutationKind kind)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
EnsureCollisionMutationThread();
|
EnsureCollisionMutationThread();
|
||||||
uint canonical = CanonicalLandblock(landblockId);
|
uint canonical = CanonicalLandblock(landblockId);
|
||||||
bool resident = Engine.IsLandblockTerrainResident(canonical);
|
if (canonical == 0u)
|
||||||
InvalidateCollisionAdmission(canonical);
|
throw new ArgumentOutOfRangeException(nameof(landblockId));
|
||||||
bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
|
if (kind is RuntimeCollisionPrefixMutationKind.Activation)
|
||||||
_suppressCollisionOwnerJournal = true;
|
throw new ArgumentOutOfRangeException(nameof(kind));
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
public RuntimeCollisionAcknowledgement WithdrawCollision(
|
if (!_collisionPrefixMutations.TryGetValue(
|
||||||
uint landblockId)
|
canonical,
|
||||||
{
|
out RuntimeCollisionPrefixMutation? mutation))
|
||||||
EnsureNotDisposed();
|
|
||||||
EnsureCollisionMutationThread();
|
|
||||||
uint canonical = CanonicalLandblock(landblockId);
|
|
||||||
bool resident = Engine.IsLandblockTerrainResident(canonical);
|
|
||||||
InvalidateCollisionAdmission(canonical);
|
|
||||||
bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
|
|
||||||
_suppressCollisionOwnerJournal = true;
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
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,
|
bool completed = SetPosition.ReleaseCollisionPrefixAfterMutation(
|
||||||
_collisionGenerations[canonical],
|
mutation.Quiescence,
|
||||||
resident,
|
mutation.TargetGeneration,
|
||||||
Ready: false);
|
mutation.Ready);
|
||||||
|
if (completed)
|
||||||
|
{
|
||||||
|
_collisionPrefixMutations.Remove(canonical);
|
||||||
|
TrimCollisionOwnerJournal();
|
||||||
|
}
|
||||||
|
return new RuntimeCollisionMutationResult(
|
||||||
|
new RuntimeCollisionAcknowledgement(
|
||||||
|
canonical,
|
||||||
|
mutation.TargetGeneration,
|
||||||
|
mutation.WasResident,
|
||||||
|
mutation.Ready),
|
||||||
|
completed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
@ -2419,6 +2762,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
_spatialRemotes.Clear();
|
_spatialRemotes.Clear();
|
||||||
_spatialProjectiles.Clear();
|
_spatialProjectiles.Clear();
|
||||||
_spatialRoots.Clear();
|
_spatialRoots.Clear();
|
||||||
|
_collisionPrefixMutations.Clear();
|
||||||
_collisionAdmissions.Clear();
|
_collisionAdmissions.Clear();
|
||||||
_collisionGenerations.Clear();
|
_collisionGenerations.Clear();
|
||||||
CellCommitted = null;
|
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)
|
internal bool TryPrepareSpatialRootAdmission(RuntimeEntityRecord record)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
|
|
@ -2960,7 +3275,13 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
{
|
{
|
||||||
return;
|
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)
|
if (subscribers.Count == 0)
|
||||||
_collisionOwnerSubscribers.Remove(ownerId);
|
_collisionOwnerSubscribers.Remove(ownerId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,8 +106,7 @@ internal readonly record struct RuntimeCollisionPrefixMutationPermission(
|
||||||
RuntimeCollisionPrefixQuiescenceToken Quiescence,
|
RuntimeCollisionPrefixQuiescenceToken Quiescence,
|
||||||
ImmutableArray<RuntimePlacementProjectionToken> Withdrawals)
|
ImmutableArray<RuntimePlacementProjectionToken> Withdrawals)
|
||||||
{
|
{
|
||||||
internal bool IsValid => Quiescence.IsValid
|
internal bool IsValid => Quiescence.IsValid;
|
||||||
&& !Withdrawals.IsDefault;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal readonly record struct RuntimeCollisionEvaluationAuthority(
|
internal readonly record struct RuntimeCollisionEvaluationAuthority(
|
||||||
|
|
@ -373,8 +372,9 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
{ get; } = [];
|
{ get; } = [];
|
||||||
internal bool ResidentsParked { get; set; }
|
internal bool ResidentsParked { get; set; }
|
||||||
internal bool PermissionIssued { get; set; }
|
internal bool PermissionIssued { get; set; }
|
||||||
internal bool AbortReleaseInProgress { get; set; }
|
internal bool ReleaseInProgress { get; set; }
|
||||||
internal ulong AbortRestoreGeneration { get; set; }
|
internal ulong ReleaseGeneration { get; set; }
|
||||||
|
internal bool ReleaseGenerationReady { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ContactCommitGuard(
|
private sealed class ContactCommitGuard(
|
||||||
|
|
@ -504,10 +504,10 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
if (_collisionPrefixQuiescence.TryGetValue(
|
if (_collisionPrefixQuiescence.TryGetValue(
|
||||||
prefix,
|
prefix,
|
||||||
out CollisionPrefixQuiescence? active)
|
out CollisionPrefixQuiescence? active)
|
||||||
&& active.AbortReleaseInProgress)
|
&& active.ReleaseInProgress)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
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(
|
_collisionPrefixQuiescence.Remove(
|
||||||
prefix,
|
prefix,
|
||||||
|
|
@ -592,7 +592,9 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
current.PermissionIssued = true;
|
current.PermissionIssued = true;
|
||||||
permission = new RuntimeCollisionPrefixMutationPermission(
|
permission = new RuntimeCollisionPrefixMutationPermission(
|
||||||
current.Token,
|
current.Token,
|
||||||
current.RetainedWithdrawals.ToImmutableArray());
|
current.RetainedWithdrawals.Count == 0
|
||||||
|
? default
|
||||||
|
: current.RetainedWithdrawals.ToImmutableArray());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -635,39 +637,93 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
return removedBeforePark;
|
return removedBeforePark;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (successorGeneration == 0UL
|
if (successorGeneration == 0UL)
|
||||||
|| !successorReady
|
return false;
|
||||||
|| current.PendingWithdrawals.Count != 0)
|
|
||||||
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!current.AbortReleaseInProgress)
|
CollisionPrefixQuiescence current = state!;
|
||||||
|
if (current.PendingWithdrawals.Count != 0)
|
||||||
|
return false;
|
||||||
|
if (requireMutationPermission
|
||||||
|
&& !current.PermissionIssued
|
||||||
|
&& !current.ReleaseInProgress)
|
||||||
{
|
{
|
||||||
current.AbortReleaseInProgress = true;
|
return false;
|
||||||
current.AbortRestoreGeneration = successorGeneration;
|
|
||||||
current.PermissionIssued = false;
|
|
||||||
RebindQuiescedDeferredOperations(
|
|
||||||
token,
|
|
||||||
successorGeneration,
|
|
||||||
ready: true);
|
|
||||||
}
|
}
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A re-entrant/network placement may have joined the still-closed
|
// A re-entrant/network placement may have joined the still-closed
|
||||||
// prefix after abort release started. Transfer every exact newcomer
|
// prefix after release started. Transfer every exact newcomer on each
|
||||||
// on each poll before deciding the barrier can open.
|
// poll before deciding the barrier can open.
|
||||||
RebindQuiescedDeferredOperations(
|
if (_operations.Count != 0)
|
||||||
token,
|
{
|
||||||
successorGeneration,
|
RebindQuiescedDeferredOperations(
|
||||||
ready: true);
|
token,
|
||||||
|
ready ? generation : 0UL,
|
||||||
|
ready,
|
||||||
|
releaseUnavailable: !ready);
|
||||||
|
}
|
||||||
|
|
||||||
if (current.PendingRestorePlacements.Count != 0
|
if (current.PendingRestorePlacements.Count != 0
|
||||||
|| HasQuiescedDeferredOperations(token.LandblockPrefix))
|
|| HasQuiescedDeferredOperations(token.LandblockPrefix))
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
bool removed = _collisionPrefixQuiescence.Remove(
|
bool removed = _collisionPrefixQuiescence.Remove(
|
||||||
token.LandblockPrefix);
|
token.LandblockPrefix);
|
||||||
if (removed)
|
if (removed)
|
||||||
|
|
@ -675,19 +731,6 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
return removed;
|
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)
|
internal void BindEventStream(RuntimeEntityObjectEventStream events)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
|
|
@ -770,6 +813,34 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
portal,
|
portal,
|
||||||
captureMoverPreparationAuthority: true);
|
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(
|
private RuntimeEntityPlacementToken BeginAcceptedPlacementCore(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
ulong expectedPositionAuthorityVersion,
|
ulong expectedPositionAuthorityVersion,
|
||||||
|
|
@ -2531,6 +2602,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
uint prefix,
|
uint prefix,
|
||||||
bool includeOutdoorCells)
|
bool includeOutdoorCells)
|
||||||
{
|
{
|
||||||
|
if (_physics.SpatialRootCount == 0)
|
||||||
|
return false;
|
||||||
var roots = new List<RuntimeEntityRecord>();
|
var roots = new List<RuntimeEntityRecord>();
|
||||||
_physics.CopySpatialRootsTo(roots);
|
_physics.CopySpatialRootsTo(roots);
|
||||||
for (int index = 0; index < roots.Count; index++)
|
for (int index = 0; index < roots.Count; index++)
|
||||||
|
|
@ -2586,7 +2659,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
uint prefix = state.Token.LandblockPrefix;
|
uint prefix = state.Token.LandblockPrefix;
|
||||||
foreach (Operation operation in _operations.Values)
|
foreach (Operation operation in _operations.Values)
|
||||||
{
|
{
|
||||||
if (operation.WakeableLostCell)
|
if (operation.WakeableLostCell || operation.DormantLocalActivation)
|
||||||
continue;
|
continue;
|
||||||
if (PlacementTouchesPrefix(operation.Command.Physics, prefix)
|
if (PlacementTouchesPrefix(operation.Command.Physics, prefix)
|
||||||
|| ResultTouchesPrefix(operation.Result, prefix)
|
|| ResultTouchesPrefix(operation.Result, prefix)
|
||||||
|
|
@ -2703,7 +2776,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
|| !_collisionPrefixQuiescence.TryGetValue(
|
|| !_collisionPrefixQuiescence.TryGetValue(
|
||||||
operation.CollisionPrefix,
|
operation.CollisionPrefix,
|
||||||
out CollisionPrefixQuiescence? state)
|
out CollisionPrefixQuiescence? state)
|
||||||
|| !state.AbortReleaseInProgress)
|
|| !state.ReleaseInProgress
|
||||||
|
|| !state.ReleaseGenerationReady)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2745,21 +2819,40 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
private void RebindQuiescedDeferredOperations(
|
private void RebindQuiescedDeferredOperations(
|
||||||
in RuntimeCollisionPrefixQuiescenceToken token,
|
in RuntimeCollisionPrefixQuiescenceToken token,
|
||||||
ulong successorGeneration,
|
ulong successorGeneration,
|
||||||
bool ready)
|
bool ready,
|
||||||
|
bool releaseUnavailable = false)
|
||||||
{
|
{
|
||||||
foreach (Operation operation in _operations.Values.ToArray())
|
foreach (Operation operation in _operations.Values.ToArray())
|
||||||
{
|
{
|
||||||
|
bool unavailableAfterReadyCommit = ready
|
||||||
|
&& operation.CollisionQuiescenceHeld
|
||||||
|
&& operation.CollisionGeneration == 0UL
|
||||||
|
&& operation.CollisionPrefix == token.LandblockPrefix;
|
||||||
if (!operation.WakeableLostCell
|
if (!operation.WakeableLostCell
|
||||||
|| operation.CollisionGeneration != token.CollisionGeneration
|
|| !operation.CollisionQuiescenceHeld
|
||||||
|| operation.CollisionPrefix != token.LandblockPrefix)
|
|| operation.CollisionPrefix != token.LandblockPrefix
|
||||||
|
|| (operation.CollisionGeneration
|
||||||
|
!= token.CollisionGeneration
|
||||||
|
&& !unavailableAfterReadyCommit))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
UnindexDeferred(operation);
|
UnindexDeferred(operation);
|
||||||
operation.CollisionGeneration = successorGeneration;
|
ulong reboundGeneration = releaseUnavailable
|
||||||
|
|| unavailableAfterReadyCommit
|
||||||
|
? 0UL
|
||||||
|
: successorGeneration;
|
||||||
|
operation.CollisionGeneration = reboundGeneration;
|
||||||
operation.CollisionGenerationReady = ready
|
operation.CollisionGenerationReady = ready
|
||||||
&& successorGeneration != 0UL;
|
&& reboundGeneration != 0UL;
|
||||||
if (successorGeneration != 0UL)
|
if (releaseUnavailable || unavailableAfterReadyCommit)
|
||||||
|
{
|
||||||
|
operation.CollisionQuiescenceHeld = false;
|
||||||
|
operation.Stage = operation.RequiresPreparation
|
||||||
|
? RuntimeEntityPlacementStage.AwaitingPreparation
|
||||||
|
: RuntimeEntityPlacementStage.AwaitingCell;
|
||||||
|
}
|
||||||
|
if (reboundGeneration != 0UL)
|
||||||
IndexDeferred(operation);
|
IndexDeferred(operation);
|
||||||
else
|
else
|
||||||
IndexUnboundDeferred(operation);
|
IndexUnboundDeferred(operation);
|
||||||
|
|
@ -3073,26 +3166,27 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
operation.Command.Physics,
|
operation.Command.Physics,
|
||||||
out CollisionPrefixQuiescence? blocking))
|
out CollisionPrefixQuiescence? blocking))
|
||||||
{
|
{
|
||||||
if (blocking!.AbortReleaseInProgress
|
if (blocking!.ReleaseInProgress
|
||||||
|
&& blocking.ReleaseGenerationReady
|
||||||
&& operation.CollisionQuiescenceHeld
|
&& operation.CollisionQuiescenceHeld
|
||||||
&& operation.CollisionPrefix
|
&& operation.CollisionPrefix
|
||||||
== blocking.Token.LandblockPrefix)
|
== blocking.Token.LandblockPrefix)
|
||||||
{
|
{
|
||||||
// The old collision generation remains active. Keep the
|
// The selected collision generation is active. Keep the
|
||||||
// admission barrier closed to new commands while this exact
|
// admission barrier closed while this exact parked operation
|
||||||
// parked operation restores and its Place receipt drains.
|
// restores and its Place receipt drains.
|
||||||
restoringQuiescence = blocking.Token;
|
restoringQuiescence = blocking.Token;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UnindexDeferred(operation);
|
UnindexDeferred(operation);
|
||||||
operation.CollisionPrefix = blocking.Token.LandblockPrefix;
|
operation.CollisionPrefix = blocking.Token.LandblockPrefix;
|
||||||
operation.CollisionGeneration = blocking.Token.CollisionGeneration;
|
operation.CollisionGeneration = blocking.Token.CollisionGeneration;
|
||||||
operation.CollisionGenerationReady = false;
|
operation.CollisionGenerationReady = false;
|
||||||
operation.CollisionQuiescenceHeld = true;
|
operation.CollisionQuiescenceHeld = true;
|
||||||
operation.Stage = RuntimeEntityPlacementStage.QuiescenceHeld;
|
operation.Stage = RuntimeEntityPlacementStage.QuiescenceHeld;
|
||||||
IndexDeferred(operation);
|
IndexDeferred(operation);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,13 @@ using System.Collections.Immutable;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
|
using AcDream.Core.Net;
|
||||||
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Physics;
|
||||||
|
using AcDream.Runtime.Session;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
|
|
@ -19,6 +23,91 @@ public sealed class LandblockPhysicsPublisherTests
|
||||||
private static readonly float[] HeightTable =
|
private static readonly float[] HeightTable =
|
||||||
Enumerable.Range(0, 256).Select(index => (float)index).ToArray();
|
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]
|
[Fact]
|
||||||
public void Constructor_ClonesHeightTableAndRejectsIncompleteInput()
|
public void Constructor_ClonesHeightTableAndRejectsIncompleteInput()
|
||||||
{
|
{
|
||||||
|
|
@ -821,6 +910,151 @@ public sealed class LandblockPhysicsPublisherTests
|
||||||
LandblockBuild build) =>
|
LandblockBuild build) =>
|
||||||
publisher.BeginPublication(RenderReceipt(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<FlatCollisionSphere>.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<CreateObject.AnimPartChange>(),
|
||||||
|
Array.Empty<CreateObject.TextureChange>(),
|
||||||
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||||
|
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)
|
private static LandblockRenderPublication RenderReceipt(LandblockBuild build)
|
||||||
{
|
{
|
||||||
var publisher = new LandblockRenderPublisher(
|
var publisher = new LandblockRenderPublisher(
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,106 @@ namespace AcDream.App.Tests.Streaming;
|
||||||
|
|
||||||
public sealed class LandblockRetirementCoordinatorTests
|
public sealed class LandblockRetirementCoordinatorTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void PendingPhysicsStageYieldsBeforeTerrainAndRemainsRetryable()
|
||||||
|
{
|
||||||
|
const uint landblockId = 0x2020FFFFu;
|
||||||
|
var ticket = new LandblockRetirementTicket(
|
||||||
|
new GpuLandblockRetirement(
|
||||||
|
landblockId,
|
||||||
|
LandblockRetirementKind.Full,
|
||||||
|
Array.Empty<WorldEntity>()),
|
||||||
|
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]
|
[Fact]
|
||||||
public void EntityStageFailure_DetachesImmediately_AndResumesAtFailedEntity()
|
public void EntityStageFailure_DetachesImmediately_AndResumesAtFailedEntity()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Reflection;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
@ -605,7 +607,7 @@ public sealed class HeadlessSessionHostTests
|
||||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
RuntimePhysicsState physics = lifetime.Physics;
|
RuntimePhysicsState physics = lifetime.Physics;
|
||||||
const uint landblockId = 0xA9B4FFFFu;
|
const uint landblockId = 0xA9B4FFFFu;
|
||||||
_ = HeadlessCollisionGenerationTransaction.Execute(
|
CompleteCollisionGeneration(
|
||||||
physics,
|
physics,
|
||||||
landblockId,
|
landblockId,
|
||||||
afterAdmission: null,
|
afterAdmission: null,
|
||||||
|
|
@ -616,7 +618,7 @@ public sealed class HeadlessSessionHostTests
|
||||||
CollisionAssets(landblockId, 10f)));
|
CollisionAssets(landblockId, 10f)));
|
||||||
|
|
||||||
Assert.Throws<FixtureCollisionPublicationException>(() =>
|
Assert.Throws<FixtureCollisionPublicationException>(() =>
|
||||||
HeadlessCollisionGenerationTransaction.Execute(
|
CompleteCollisionGeneration(
|
||||||
physics,
|
physics,
|
||||||
landblockId,
|
landblockId,
|
||||||
_ => throw new FixtureCollisionPublicationException(),
|
_ => throw new FixtureCollisionPublicationException(),
|
||||||
|
|
@ -629,13 +631,137 @@ public sealed class HeadlessSessionHostTests
|
||||||
Assert.Equal(0, ownership.CollisionAdmissionCount);
|
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]
|
[Fact]
|
||||||
public void CollisionTransactionCancelsStagingFaultWithoutWithdrawingActiveWorld()
|
public void CollisionTransactionCancelsStagingFaultWithoutWithdrawingActiveWorld()
|
||||||
{
|
{
|
||||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
RuntimePhysicsState physics = lifetime.Physics;
|
RuntimePhysicsState physics = lifetime.Physics;
|
||||||
const uint landblockId = 0xA9B4FFFFu;
|
const uint landblockId = 0xA9B4FFFFu;
|
||||||
_ = HeadlessCollisionGenerationTransaction.Execute(
|
CompleteCollisionGeneration(
|
||||||
physics,
|
physics,
|
||||||
landblockId,
|
landblockId,
|
||||||
afterAdmission: null,
|
afterAdmission: null,
|
||||||
|
|
@ -646,7 +772,7 @@ public sealed class HeadlessSessionHostTests
|
||||||
CollisionAssets(landblockId, 10f)));
|
CollisionAssets(landblockId, 10f)));
|
||||||
|
|
||||||
Assert.Throws<FixtureCollisionPublicationException>(() =>
|
Assert.Throws<FixtureCollisionPublicationException>(() =>
|
||||||
HeadlessCollisionGenerationTransaction.Execute(
|
CompleteCollisionGeneration(
|
||||||
physics,
|
physics,
|
||||||
landblockId,
|
landblockId,
|
||||||
afterAdmission: null,
|
afterAdmission: null,
|
||||||
|
|
@ -714,6 +840,107 @@ public sealed class HeadlessSessionHostTests
|
||||||
runtime.MovementOwner.Controller = controller;
|
runtime.MovementOwner.Controller = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void CompleteCollisionGeneration(
|
||||||
|
RuntimePhysicsState physics,
|
||||||
|
uint landblockId,
|
||||||
|
Action<RuntimeCollisionAdmission>? afterAdmission,
|
||||||
|
Action<RuntimeCollisionAdmission,
|
||||||
|
PreparedLandblockCollisionGeneration> 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<FlatCollisionSphere>.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(
|
private static RuntimeLandblockCollisionAssets CollisionAssets(
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
float terrainHeight)
|
float terrainHeight)
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,33 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
.IsExactPreparedPlacementCurrent(
|
.IsExactPreparedPlacementCurrent(
|
||||||
fixture.Record,
|
fixture.Record,
|
||||||
fixture.Placement,
|
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<InvalidOperationException>(() => 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]
|
[Fact]
|
||||||
|
|
@ -2467,32 +2493,48 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
RuntimeCollisionAdmission admission,
|
RuntimeCollisionAdmission admission,
|
||||||
PreparedLandblockCollisionGeneration prepared)
|
PreparedLandblockCollisionGeneration prepared)
|
||||||
{
|
{
|
||||||
while (true)
|
bool engineCommitted = false;
|
||||||
|
for (int poll = 0; poll < 10_000; poll++)
|
||||||
{
|
{
|
||||||
while (!physics.AdvanceCollisionRetainedOwnerCapture(
|
if (!engineCommitted)
|
||||||
admission,
|
|
||||||
prepared).Completed)
|
|
||||||
{
|
{
|
||||||
|
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(
|
Assert.True(physics.SetPosition.AcknowledgeProjection(
|
||||||
admission,
|
projection.Token));
|
||||||
prepared,
|
|
||||||
ownerId);
|
|
||||||
}
|
}
|
||||||
RuntimeCollisionSealStep seal;
|
engineCommitted = result.EngineCommitted;
|
||||||
do
|
|
||||||
{
|
|
||||||
seal = physics.AdvanceCollisionGenerationSeal(
|
|
||||||
admission,
|
|
||||||
prepared);
|
|
||||||
}
|
|
||||||
while (!seal.Completed && !seal.Restarted);
|
|
||||||
if (seal.Completed)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return physics.CommitCollisionGeneration(admission, prepared);
|
throw new InvalidOperationException(
|
||||||
|
"Collision generation did not complete its Runtime mutation transaction.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RuntimeLandblockCollisionAssets CollisionAssets(
|
private static RuntimeLandblockCollisionAssets CollisionAssets(
|
||||||
|
|
|
||||||
|
|
@ -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<CellSurface>(),
|
||||||
|
Array.Empty<PortalPlane>(),
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0u));
|
||||||
|
_ = SealMutation(physics, admission, prepared);
|
||||||
|
return prepared;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint[] SealMutation(
|
||||||
|
RuntimePhysicsState physics,
|
||||||
|
RuntimeCollisionAdmission admission,
|
||||||
|
PreparedLandblockCollisionGeneration prepared)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
while (!physics.AdvanceCollisionRetainedOwnerCapture(
|
||||||
|
admission,
|
||||||
|
prepared).Completed)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
foreach (uint ownerId in prepared.RetainedOwnerIds)
|
||||||
|
physics.RefreshCollisionRetainedOwner(admission, prepared, ownerId);
|
||||||
|
RuntimeCollisionSealStep seal;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
seal = physics.AdvanceCollisionGenerationSeal(
|
||||||
|
admission,
|
||||||
|
prepared);
|
||||||
|
}
|
||||||
|
while (!seal.Completed && !seal.Restarted);
|
||||||
|
if (seal.Completed)
|
||||||
|
return [.. prepared.RetainedOwnerIds];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ using DatReaderWriter.Types;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Tests.Physics;
|
namespace AcDream.Runtime.Tests.Physics;
|
||||||
|
|
||||||
public sealed class RuntimeCollisionPrefixQuiescenceTests
|
public sealed partial class RuntimeCollisionPrefixQuiescenceTests
|
||||||
{
|
{
|
||||||
private const uint PrefixP = 0xA9B40000u;
|
private const uint PrefixP = 0xA9B40000u;
|
||||||
private const uint CellP = PrefixP | 0x0001u;
|
private const uint CellP = PrefixP | 0x0001u;
|
||||||
|
|
@ -68,7 +68,10 @@ public sealed class RuntimeCollisionPrefixQuiescenceTests
|
||||||
Assert.True(fixture.Lifetime.Physics
|
Assert.True(fixture.Lifetime.Physics
|
||||||
.IsCollisionPrefixMutationPermissionCurrent(permission));
|
.IsCollisionPrefixMutationPermissionCurrent(permission));
|
||||||
Assert.True(fixture.Lifetime.Physics
|
Assert.True(fixture.Lifetime.Physics
|
||||||
.CompleteCollisionPrefixQuiescence(permission));
|
.CancelCollisionPrefixQuiescence(
|
||||||
|
token,
|
||||||
|
successorGeneration: 2UL,
|
||||||
|
successorReady: false));
|
||||||
Assert.False(fixture.Lifetime.Physics
|
Assert.False(fixture.Lifetime.Physics
|
||||||
.IsCollisionPrefixMutationPermissionCurrent(permission));
|
.IsCollisionPrefixMutationPermissionCurrent(permission));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<InternalsVisibleToAttribute>()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -336,7 +336,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
CommitPrepared(first.Physics, newer, prepared));
|
CommitPrepared(first.Physics, newer, prepared));
|
||||||
|
|
||||||
RuntimeCollisionAcknowledgement withdrawn =
|
RuntimeCollisionAcknowledgement withdrawn =
|
||||||
first.Physics.WithdrawCollision(0xA9B4FFFFu);
|
CompleteWithdrawal(first.Physics, 0xA9B4FFFFu)
|
||||||
|
.Acknowledgement;
|
||||||
Assert.True(withdrawn.WasResident);
|
Assert.True(withdrawn.WasResident);
|
||||||
Assert.True(withdrawn.Generation > completed.Generation);
|
Assert.True(withdrawn.Generation > completed.Generation);
|
||||||
Assert.Equal(0, first.Physics.Engine.LandblockCount);
|
Assert.Equal(0, first.Physics.Engine.LandblockCount);
|
||||||
|
|
@ -465,7 +466,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
uint owner = Assert.Single(SealPrepared(physics, admission, prepared));
|
uint owner = Assert.Single(SealPrepared(physics, admission, prepared));
|
||||||
physics.Engine.ShadowObjects.UpdatePhysicsState(owner, 0x14u);
|
physics.Engine.ShadowObjects.UpdatePhysicsState(owner, 0x14u);
|
||||||
|
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f));
|
Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f));
|
||||||
|
|
@ -561,6 +563,11 @@ public sealed class RuntimePhysicsStateTests
|
||||||
Assert.True(sealSteps > ownerCount);
|
Assert.True(sealSteps > ownerCount);
|
||||||
Assert.True(workUnits > ownerCount);
|
Assert.True(workUnits > ownerCount);
|
||||||
|
|
||||||
|
Assert.False(physics.CommitCollisionGeneration(
|
||||||
|
admission,
|
||||||
|
prepared).Committed);
|
||||||
|
_ = SealPrepared(physics, admission, prepared);
|
||||||
|
|
||||||
_ = GC.GetAllocatedBytesForCurrentThread();
|
_ = GC.GetAllocatedBytesForCurrentThread();
|
||||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
RuntimeCollisionGenerationCommit commit =
|
RuntimeCollisionGenerationCommit commit =
|
||||||
|
|
@ -662,7 +669,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
}
|
}
|
||||||
while (!seal.Completed);
|
while (!seal.Completed);
|
||||||
|
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f));
|
Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f));
|
||||||
|
|
@ -742,7 +750,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
ShadowEntry[] entries = physics.Engine.ShadowObjects
|
ShadowEntry[] entries = physics.Engine.ShadowObjects
|
||||||
|
|
@ -841,7 +850,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
}
|
}
|
||||||
while (!seal.Completed);
|
while (!seal.Completed);
|
||||||
|
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
ShadowEntry[] entries = physics.Engine.ShadowObjects
|
ShadowEntry[] entries = physics.Engine.ShadowObjects
|
||||||
|
|
@ -885,6 +895,10 @@ public sealed class RuntimePhysicsStateTests
|
||||||
ShadowObjectRegistry shadowFacade = physics.Engine.ShadowObjects;
|
ShadowObjectRegistry shadowFacade = physics.Engine.ShadowObjects;
|
||||||
int notifications = 0;
|
int notifications = 0;
|
||||||
physics.CollisionGenerationCommitted += _ => notifications++;
|
physics.CollisionGenerationCommitted += _ => notifications++;
|
||||||
|
Assert.False(physics.CommitCollisionGeneration(
|
||||||
|
admission,
|
||||||
|
prepared).Committed);
|
||||||
|
_ = SealPrepared(physics, admission, prepared);
|
||||||
_ = GC.GetAllocatedBytesForCurrentThread();
|
_ = GC.GetAllocatedBytesForCurrentThread();
|
||||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
RuntimeCollisionGenerationCommit commit =
|
RuntimeCollisionGenerationCommit commit =
|
||||||
|
|
@ -1049,6 +1063,10 @@ public sealed class RuntimePhysicsStateTests
|
||||||
PhysicsDataCache cacheFacade = physics.DataCache;
|
PhysicsDataCache cacheFacade = physics.DataCache;
|
||||||
CellGraph graphFacade = physics.DataCache.CellGraph;
|
CellGraph graphFacade = physics.DataCache.CellGraph;
|
||||||
ShadowObjectRegistry shadowFacade = physics.Engine.ShadowObjects;
|
ShadowObjectRegistry shadowFacade = physics.Engine.ShadowObjects;
|
||||||
|
Assert.False(physics.CommitCollisionGeneration(
|
||||||
|
admission,
|
||||||
|
prepared).Committed);
|
||||||
|
_ = SealPrepared(physics, admission, prepared);
|
||||||
_ = GC.GetAllocatedBytesForCurrentThread();
|
_ = GC.GetAllocatedBytesForCurrentThread();
|
||||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
RuntimeCollisionGenerationCommit commit =
|
RuntimeCollisionGenerationCommit commit =
|
||||||
|
|
@ -1106,9 +1124,15 @@ public sealed class RuntimePhysicsStateTests
|
||||||
_ = SealPrepared(physics, firstAdmission, first);
|
_ = SealPrepared(physics, firstAdmission, first);
|
||||||
_ = SealPrepared(physics, secondAdmission, second);
|
_ = SealPrepared(physics, secondAdmission, second);
|
||||||
|
|
||||||
|
Assert.False(physics.CommitCollisionGeneration(
|
||||||
|
firstAdmission,
|
||||||
|
first).Committed);
|
||||||
|
_ = SealPrepared(physics, firstAdmission, first);
|
||||||
|
|
||||||
_ = GC.GetAllocatedBytesForCurrentThread();
|
_ = GC.GetAllocatedBytesForCurrentThread();
|
||||||
long firstBefore = GC.GetAllocatedBytesForCurrentThread();
|
long firstBefore = GC.GetAllocatedBytesForCurrentThread();
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
firstAdmission,
|
firstAdmission,
|
||||||
first).Committed);
|
first).Committed);
|
||||||
long firstAllocated =
|
long firstAllocated =
|
||||||
|
|
@ -1118,10 +1142,15 @@ public sealed class RuntimePhysicsStateTests
|
||||||
Assert.True(physics.Engine.IsLandblockTerrainResident(firstLandblock));
|
Assert.True(physics.Engine.IsLandblockTerrainResident(firstLandblock));
|
||||||
Assert.False(physics.Engine.IsLandblockTerrainResident(secondLandblock));
|
Assert.False(physics.Engine.IsLandblockTerrainResident(secondLandblock));
|
||||||
|
|
||||||
|
_ = SealPrepared(physics, secondAdmission, second);
|
||||||
|
Assert.False(physics.CommitCollisionGeneration(
|
||||||
|
secondAdmission,
|
||||||
|
second).Committed);
|
||||||
_ = SealPrepared(physics, secondAdmission, second);
|
_ = SealPrepared(physics, secondAdmission, second);
|
||||||
_ = GC.GetAllocatedBytesForCurrentThread();
|
_ = GC.GetAllocatedBytesForCurrentThread();
|
||||||
long secondBefore = GC.GetAllocatedBytesForCurrentThread();
|
long secondBefore = GC.GetAllocatedBytesForCurrentThread();
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
secondAdmission,
|
secondAdmission,
|
||||||
second).Committed);
|
second).Committed);
|
||||||
long secondAllocated =
|
long secondAllocated =
|
||||||
|
|
@ -1172,7 +1201,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
_ = SealPrepared(physics, firstAdmission, first);
|
_ = SealPrepared(physics, firstAdmission, first);
|
||||||
_ = SealPrepared(physics, secondAdmission, second);
|
_ = SealPrepared(physics, secondAdmission, second);
|
||||||
|
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
firstAdmission,
|
firstAdmission,
|
||||||
first).Committed);
|
first).Committed);
|
||||||
physics.Engine.ShadowObjects.UpdatePosition(
|
physics.Engine.ShadowObjects.UpdatePosition(
|
||||||
|
|
@ -1185,7 +1215,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
seedCellId: 0x01010001u);
|
seedCellId: 0x01010001u);
|
||||||
|
|
||||||
_ = SealPrepared(physics, secondAdmission, second);
|
_ = SealPrepared(physics, secondAdmission, second);
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
secondAdmission,
|
secondAdmission,
|
||||||
second).Committed);
|
second).Committed);
|
||||||
Assert.Equal(12f, Assert.Single(
|
Assert.Equal(12f, Assert.Single(
|
||||||
|
|
@ -1256,11 +1287,13 @@ public sealed class RuntimePhysicsStateTests
|
||||||
_ = SealPrepared(physics, northAdmission, northPrepared);
|
_ = SealPrepared(physics, northAdmission, northPrepared);
|
||||||
_ = SealPrepared(physics, southAdmission, southPrepared);
|
_ = SealPrepared(physics, southAdmission, southPrepared);
|
||||||
|
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
northAdmission,
|
northAdmission,
|
||||||
northPrepared).Committed);
|
northPrepared).Committed);
|
||||||
_ = SealPrepared(physics, southAdmission, southPrepared);
|
_ = SealPrepared(physics, southAdmission, southPrepared);
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
southAdmission,
|
southAdmission,
|
||||||
southPrepared).Committed);
|
southPrepared).Committed);
|
||||||
Assert.True(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
|
Assert.True(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
|
||||||
|
|
@ -1394,7 +1427,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
|
|
||||||
physics.Engine.UpdatePlayerCurrCell(secondCell);
|
physics.Engine.UpdatePlayerCurrCell(secondCell);
|
||||||
ObjCell oldRootCell = physics.DataCache.CellGraph.CurrCell!;
|
ObjCell oldRootCell = physics.DataCache.CellGraph.CurrCell!;
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
|
|
||||||
|
|
@ -1454,7 +1488,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
destination,
|
destination,
|
||||||
seedCellId: 0x02020001u);
|
seedCellId: 0x02020001u);
|
||||||
|
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
Assert.False(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
|
Assert.False(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
|
||||||
|
|
@ -1521,7 +1556,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
seedCellId: 0x02020001u,
|
seedCellId: 0x02020001u,
|
||||||
isStatic: false);
|
isStatic: false);
|
||||||
|
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
ShadowEntry entry = Assert.Single(
|
ShadowEntry entry = Assert.Single(
|
||||||
|
|
@ -1692,8 +1728,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
preparedThird,
|
preparedThird,
|
||||||
CollisionAssets(replacement));
|
CollisionAssets(replacement));
|
||||||
|
|
||||||
Assert.True(physics.DemoteCollisionToTerrain(demoted).WasResident);
|
Assert.True(CompleteDemotion(physics, demoted).WasResident);
|
||||||
Assert.True(physics.WithdrawCollision(withdrawn).WasResident);
|
Assert.True(CompleteWithdrawal(physics, withdrawn).WasResident);
|
||||||
Assert.True(CommitPrepared(
|
Assert.True(CommitPrepared(
|
||||||
physics,
|
physics,
|
||||||
third,
|
third,
|
||||||
|
|
@ -1745,7 +1781,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = SealPrepared(physics, retiredAdmission, retiredPrepared);
|
_ = SealPrepared(physics, retiredAdmission, retiredPrepared);
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
retiredAdmission,
|
retiredAdmission,
|
||||||
retiredPrepared).Committed);
|
retiredPrepared).Committed);
|
||||||
if (beginRebase)
|
if (beginRebase)
|
||||||
|
|
@ -1762,9 +1799,9 @@ public sealed class RuntimePhysicsStateTests
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withdraw)
|
if (withdraw)
|
||||||
Assert.True(physics.WithdrawCollision(retired).WasResident);
|
Assert.True(CompleteWithdrawal(physics, retired).WasResident);
|
||||||
else
|
else
|
||||||
Assert.True(physics.DemoteCollisionToTerrain(retired).WasResident);
|
Assert.True(CompleteDemotion(physics, retired).WasResident);
|
||||||
|
|
||||||
Assert.True(CommitPrepared(
|
Assert.True(CommitPrepared(
|
||||||
physics,
|
physics,
|
||||||
|
|
@ -1986,6 +2023,11 @@ public sealed class RuntimePhysicsStateTests
|
||||||
while (!seal.Completed);
|
while (!seal.Completed);
|
||||||
Assert.Equal(ownerCount, worked);
|
Assert.Equal(ownerCount, worked);
|
||||||
|
|
||||||
|
Assert.False(physics.CommitCollisionGeneration(
|
||||||
|
admission,
|
||||||
|
prepared).Committed);
|
||||||
|
_ = SealPrepared(physics, admission, prepared);
|
||||||
|
|
||||||
_ = GC.GetAllocatedBytesForCurrentThread();
|
_ = GC.GetAllocatedBytesForCurrentThread();
|
||||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
RuntimeCollisionGenerationCommit commit =
|
RuntimeCollisionGenerationCommit commit =
|
||||||
|
|
@ -2050,7 +2092,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
survivingTarget,
|
survivingTarget,
|
||||||
seedCellId: 0x02020001u,
|
seedCellId: 0x02020001u,
|
||||||
isStatic: false);
|
isStatic: false);
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
survivingAdmission,
|
survivingAdmission,
|
||||||
surviving).Committed);
|
surviving).Committed);
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
|
|
@ -2097,7 +2140,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
0f,
|
0f,
|
||||||
target,
|
target,
|
||||||
seedCellId: 0x01010001u);
|
seedCellId: 0x01010001u);
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
Assert.True(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
|
Assert.True(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
|
||||||
|
|
@ -2146,7 +2190,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
outside,
|
outside,
|
||||||
seedCellId: 0x02020001u);
|
seedCellId: 0x02020001u);
|
||||||
physics.Engine.ShadowObjects.UpdatePhysicsState(ownerId, 0x55u);
|
physics.Engine.ShadowObjects.UpdatePhysicsState(ownerId, 0x55u);
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
ShadowEntry entry = Assert.Single(
|
ShadowEntry entry = Assert.Single(
|
||||||
|
|
@ -2217,7 +2262,7 @@ public sealed class RuntimePhysicsStateTests
|
||||||
replacementAdmission,
|
replacementAdmission,
|
||||||
replacementPrepared);
|
replacementPrepared);
|
||||||
|
|
||||||
Assert.True(physics.WithdrawCollision(retired).WasResident);
|
Assert.True(CompleteWithdrawal(physics, retired).WasResident);
|
||||||
Assert.False(physics.CommitCollisionGeneration(
|
Assert.False(physics.CommitCollisionGeneration(
|
||||||
replacementAdmission,
|
replacementAdmission,
|
||||||
replacementPrepared).Committed);
|
replacementPrepared).Committed);
|
||||||
|
|
@ -2226,7 +2271,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
physics,
|
physics,
|
||||||
replacementAdmission,
|
replacementAdmission,
|
||||||
replacementPrepared);
|
replacementPrepared);
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
replacementAdmission,
|
replacementAdmission,
|
||||||
replacementPrepared).Committed);
|
replacementPrepared).Committed);
|
||||||
Assert.False(physics.Engine.IsLandblockTerrainResident(retired));
|
Assert.False(physics.Engine.IsLandblockTerrainResident(retired));
|
||||||
|
|
@ -2254,7 +2300,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
uint ordinal = index + 0x1000u;
|
uint ordinal = index + 0x1000u;
|
||||||
uint x = ordinal & 0xFFu;
|
uint x = ordinal & 0xFFu;
|
||||||
uint y = (ordinal >> 8) & 0xFFu;
|
uint y = (ordinal >> 8) & 0xFFu;
|
||||||
_ = physics.WithdrawCollision(
|
_ = CompleteWithdrawal(
|
||||||
|
physics,
|
||||||
(x << 24) | (y << 16) | 0xFFFFu);
|
(x << 24) | (y << 16) | 0xFFFFu);
|
||||||
}
|
}
|
||||||
Assert.False(physics.CommitCollisionGeneration(
|
Assert.False(physics.CommitCollisionGeneration(
|
||||||
|
|
@ -2272,7 +2319,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
Assert.True(++steps < 100_000);
|
Assert.True(++steps < 100_000);
|
||||||
}
|
}
|
||||||
while (!seal.Completed);
|
while (!seal.Completed);
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
}
|
}
|
||||||
|
|
@ -2380,7 +2428,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
Assert.InRange(seal.WorkUnits, 0, 1);
|
Assert.InRange(seal.WorkUnits, 0, 1);
|
||||||
}
|
}
|
||||||
while (!seal.Completed);
|
while (!seal.Completed);
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug());
|
Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug());
|
||||||
|
|
@ -2448,7 +2497,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
seedCellId: 0x01010001u,
|
seedCellId: 0x01010001u,
|
||||||
isStatic: false);
|
isStatic: false);
|
||||||
physics.Engine.ShadowObjects.Deregister(77u);
|
physics.Engine.ShadowObjects.Deregister(77u);
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug());
|
Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug());
|
||||||
|
|
@ -2484,7 +2534,8 @@ public sealed class RuntimePhysicsStateTests
|
||||||
seedCellId: 0x01010001u,
|
seedCellId: 0x01010001u,
|
||||||
isStatic: false);
|
isStatic: false);
|
||||||
}
|
}
|
||||||
Assert.True(physics.CommitCollisionGeneration(
|
Assert.True(CompleteSealedCommit(
|
||||||
|
physics,
|
||||||
admission,
|
admission,
|
||||||
prepared).Committed);
|
prepared).Committed);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
|
|
@ -2903,7 +2954,55 @@ public sealed class RuntimePhysicsStateTests
|
||||||
PreparedLandblockCollisionGeneration prepared)
|
PreparedLandblockCollisionGeneration prepared)
|
||||||
{
|
{
|
||||||
_ = SealPrepared(physics, admission, 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(
|
private static uint[] SealPrepared(
|
||||||
|
|
|
||||||
|
|
@ -1750,8 +1750,6 @@ public sealed class RuntimeSetPositionStateTests
|
||||||
RuntimePlacementProjectionSnapshot placed = observer.Deltas[^1].Placement;
|
RuntimePlacementProjectionSnapshot placed = observer.Deltas[^1].Placement;
|
||||||
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
||||||
Assert.Equal(DestinationIndoorCell, placed.Token.ExactCellId);
|
Assert.Equal(DestinationIndoorCell, placed.Token.ExactCellId);
|
||||||
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
||||||
placed.Token));
|
|
||||||
RuntimePhysicsOwnershipSnapshot final = lifetime.Physics.CaptureOwnership();
|
RuntimePhysicsOwnershipSnapshot final = lifetime.Physics.CaptureOwnership();
|
||||||
Assert.Equal(0, final.DeferredSetPositionBucketCount);
|
Assert.Equal(0, final.DeferredSetPositionBucketCount);
|
||||||
Assert.Equal(0, final.UnboundDeferredSetPositionCellCount);
|
Assert.Equal(0, final.UnboundDeferredSetPositionCellCount);
|
||||||
|
|
@ -1872,10 +1870,6 @@ public sealed class RuntimeSetPositionStateTests
|
||||||
Assert.All(observer.Deltas, delta => Assert.Equal(
|
Assert.All(observer.Deltas, delta => Assert.Equal(
|
||||||
RuntimePlacementProjectionKind.Place,
|
RuntimePlacementProjectionKind.Place,
|
||||||
delta.Placement.Kind));
|
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]
|
[Theory]
|
||||||
|
|
@ -2425,32 +2419,48 @@ public sealed class RuntimeSetPositionStateTests
|
||||||
RuntimeCollisionAdmission admission,
|
RuntimeCollisionAdmission admission,
|
||||||
PreparedLandblockCollisionGeneration prepared)
|
PreparedLandblockCollisionGeneration prepared)
|
||||||
{
|
{
|
||||||
while (true)
|
bool engineCommitted = false;
|
||||||
|
for (int poll = 0; poll < 10_000; poll++)
|
||||||
{
|
{
|
||||||
while (!physics.AdvanceCollisionRetainedOwnerCapture(
|
if (!engineCommitted)
|
||||||
admission,
|
|
||||||
prepared).Completed)
|
|
||||||
{
|
{
|
||||||
|
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(
|
Assert.True(physics.SetPosition.AcknowledgeProjection(
|
||||||
admission,
|
projection.Token));
|
||||||
prepared,
|
|
||||||
ownerId);
|
|
||||||
}
|
}
|
||||||
RuntimeCollisionSealStep seal;
|
engineCommitted = result.EngineCommitted;
|
||||||
do
|
|
||||||
{
|
|
||||||
seal = physics.AdvanceCollisionGenerationSeal(
|
|
||||||
admission,
|
|
||||||
prepared);
|
|
||||||
}
|
|
||||||
while (!seal.Completed && !seal.Restarted);
|
|
||||||
if (seal.Completed)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return physics.CommitCollisionGeneration(admission, prepared);
|
throw new InvalidOperationException(
|
||||||
|
"Collision generation did not complete its Runtime mutation transaction.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddSyntheticCell(
|
private static void AddSyntheticCell(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue