harden(runtime): close canonical entity object lifetime

Retain exact teardown receipts before terminal callbacks, preserve ordered re-entrant entity/object publication, publish committed facts across projection failures, and make direct disposal converge every canonical owner. Add a complete ownership ledger plus adversarial direct/graphical parity, callback, GUID reuse, reset, and resource-churn gates.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 07:13:42 +02:00
parent 84954c8b77
commit 119b7c1151
9 changed files with 992 additions and 95 deletions

View file

@ -201,6 +201,9 @@ public sealed class ClientObjectTable
public int ObjectCount => _objects.Count;
public int ContainerCount => _containers.Count;
public int ContainerProjectionCount => _containerIndex.Count;
public int EquipmentOwnerCount => _equipmentIndex.Count;
public int PendingMoveCount => _pendingMoves.Count;
public IEnumerable<ClientObject> Objects => _objects.Values;
public IEnumerable<Container> Containers => _containers.Values;

View file

@ -17,6 +17,12 @@ public sealed class ParentAttachmentState
private readonly Dictionary<uint, ParentAttachmentRelation> _recoveryByChild = new();
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = new();
public int UnresolvedRelationCount =>
_unresolvedByChild.Values.Sum(queue => queue.Count);
public int StagedRelationCount => _stagedByChild.Count;
public int RecoveryRelationCount => _recoveryByChild.Count;
public int CommittedRelationCount => _lastAcceptedByChild.Count;
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
{
_stagedByChild[relation.ChildGuid] = relation;

View file

@ -247,6 +247,18 @@ public sealed class RuntimeEntityDirectory
record.AdvanceMovementAuthority();
}
public void AdvanceMovementCommit(RuntimeEntityRecord record)
{
EnsureKnown(record);
record.AdvanceMovementCommit();
}
public void AdvanceParentCommit(RuntimeEntityRecord record)
{
EnsureKnown(record);
record.AdvanceParentCommit();
}
public void AdvanceObjDescAuthority(RuntimeEntityRecord record)
{
EnsureKnown(record);
@ -293,8 +305,7 @@ public sealed class RuntimeEntityDirectory
uint canonicalLandblockId)
{
EnsureKnown(record);
record.FullCellId = fullCellId;
record.CanonicalLandblockId = canonicalLandblockId;
record.SetFullCell(fullCellId, canonicalLandblockId);
}
public void SetFinalPhysicsState(

View file

@ -16,8 +16,10 @@ public interface IRuntimeEntityObjectEventSource
/// <summary>
/// One synchronous, generation-stamped commit stream for the canonical entity
/// directory and retained object table. It owns no queue and dispatches on the
/// caller's update/network-drain thread.
/// directory and retained object table. It owns no cross-frame queue and
/// dispatches on the caller's update/network-drain thread. A retained
/// synchronous drain list preserves sequence order when an observer commits
/// another mutation re-entrantly.
/// </summary>
public sealed class RuntimeEntityObjectEventStream
: IRuntimeEntityObjectEventSource,
@ -27,10 +29,12 @@ public sealed class RuntimeEntityObjectEventStream
private readonly ClientObjectTable _objects;
private readonly RuntimeEventSequencer _sequencer = new();
private readonly object _observerGate = new();
private readonly List<PendingDispatch> _pendingDispatch = [];
private IRuntimeEntityObjectObserver[] _observers = [];
private Func<RuntimeGenerationToken> _generation = static () => default;
private Func<ulong> _frameNumber = static () => 0UL;
private bool _contextBound;
private bool _dispatching;
private bool _disposed;
internal RuntimeEntityObjectEventStream(
@ -49,6 +53,8 @@ public sealed class RuntimeEntityObjectEventStream
public ulong LastSequence => _sequencer.LastSequence;
public int SubscriberCount => Volatile.Read(ref _observers).Length;
public int PendingDispatchCount => _pendingDispatch.Count;
public bool IsDispatching => _dispatching;
public long DispatchFailureCount { get; private set; }
public Exception? LastDispatchFailure { get; private set; }
@ -123,19 +129,7 @@ public sealed class RuntimeEntityObjectEventStream
NextStamp(),
change,
RuntimeEntityObjectViews.Snapshot(record));
IRuntimeEntityObjectObserver[] observers =
Volatile.Read(ref _observers);
foreach (IRuntimeEntityObjectObserver observer in observers)
{
try
{
observer.OnEntity(in delta);
}
catch (Exception error)
{
RecordDispatchFailure(error);
}
}
EnqueueAndDrain(PendingDispatch.ForEntity(delta));
}
public void Dispose()
@ -154,6 +148,16 @@ public sealed class RuntimeEntityObjectEventStream
}
}
internal void DetachObservers()
{
lock (_observerGate)
{
if (_disposed)
return;
Volatile.Write(ref _observers, []);
}
}
private void OnObjectAdded(ClientObject item) =>
PublishInventory(RuntimeInventoryChange.Added, item);
@ -206,13 +210,46 @@ public sealed class RuntimeEntityObjectEventStream
NextStamp(),
change,
item);
EnqueueAndDrain(PendingDispatch.ForInventory(delta));
}
private void EnqueueAndDrain(PendingDispatch pending)
{
_pendingDispatch.Add(pending);
if (_dispatching)
return;
_dispatching = true;
try
{
for (int index = 0; index < _pendingDispatch.Count; index++)
Dispatch(_pendingDispatch[index]);
}
finally
{
_pendingDispatch.Clear();
_dispatching = false;
}
}
private void Dispatch(PendingDispatch pending)
{
IRuntimeEntityObjectObserver[] observers =
Volatile.Read(ref _observers);
foreach (IRuntimeEntityObjectObserver observer in observers)
{
try
{
observer.OnInventory(in delta);
if (pending.Kind is PendingDispatchKind.Entity)
{
RuntimeEntityDelta delta = pending.Entity;
observer.OnEntity(in delta);
}
else
{
RuntimeInventoryDelta delta = pending.Inventory;
observer.OnInventory(in delta);
}
}
catch (Exception error)
{
@ -258,6 +295,26 @@ public sealed class RuntimeEntityObjectEventStream
}
}
private enum PendingDispatchKind : byte
{
Entity,
Inventory,
}
private readonly record struct PendingDispatch(
PendingDispatchKind Kind,
RuntimeEntityDelta Entity,
RuntimeInventoryDelta Inventory)
{
public static PendingDispatch ForEntity(
RuntimeEntityDelta entity) =>
new(PendingDispatchKind.Entity, entity, default);
public static PendingDispatch ForInventory(
RuntimeInventoryDelta inventory) =>
new(PendingDispatchKind.Inventory, default, inventory);
}
private sealed class ObserverSubscription(
RuntimeEntityObjectEventStream owner,
IRuntimeEntityObjectObserver observer)

View file

@ -12,6 +12,27 @@ public readonly record struct RuntimeEntityRegistrationResult(
bool ReplacedExistingGeneration,
Exception? PriorGenerationCleanupFailure = null);
public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
int ActiveEntityCount,
int TeardownEntityCount,
int ClaimedLocalIdCount,
int AcceptedSnapshotCount,
int UnresolvedParentRelationCount,
int StagedParentRelationCount,
int RecoveryParentRelationCount,
int CommittedParentRelationCount,
int ObjectCount,
int ContainerCount,
int ContainerProjectionCount,
int EquipmentOwnerCount,
int PendingMoveCount,
int StreamSubscriberCount,
long StreamDispatchFailureCount,
bool HasLastStreamDispatchFailure,
int PendingDispatchCount,
bool IsDispatching,
bool IsSessionClearInProgress);
/// <summary>
/// One exact DeleteObject acceptance issued by a
/// <see cref="RuntimeEntityObjectLifetime"/>. The graphical host retires the
@ -66,6 +87,31 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
public IRuntimeInventoryView InventoryView { get; }
public RuntimeEntityObjectEventStream Events { get; }
public RuntimeEntityObjectOwnershipSnapshot CaptureOwnership()
{
ParentAttachmentState parents = Entities.ParentAttachments;
return new RuntimeEntityObjectOwnershipSnapshot(
Entities.Count,
Entities.PendingTeardownCount,
Entities.ClaimedLocalIdCount,
Entities.Snapshots.Count,
parents.UnresolvedRelationCount,
parents.StagedRelationCount,
parents.RecoveryRelationCount,
parents.CommittedRelationCount,
Objects.ObjectCount,
Objects.ContainerCount,
Objects.ContainerProjectionCount,
Objects.EquipmentOwnerCount,
Objects.PendingMoveCount,
Events.SubscriberCount,
Events.DispatchFailureCount,
Events.LastDispatchFailure is not null,
Events.PendingDispatchCount,
Events.IsDispatching,
_sessionClearInProgress);
}
public void BindEventContext(
Func<RuntimeGenerationToken> generation,
Func<ulong> frameNumber)
@ -181,6 +227,10 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Exception? cleanupFailure = null;
if (prior is not null)
{
// Removing the active GUID is an ownership transfer, not a gap.
// Retain the exact incarnation before arbitrary synchronous
// observers can re-enter registration, reset, or disposal.
Entities.RetainTeardown(prior);
try
{
PublishEntity(RuntimeEntityChange.Deleted, prior);
@ -343,11 +393,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.RefreshSnapshot(canonical, accepted);
Entities.AdvanceObjDescAuthority(canonical);
acknowledgeProjection?.Invoke(canonical);
if (!Entities.IsCurrent(canonical))
return false;
PublishEntity(RuntimeEntityChange.Updated, canonical);
return Entities.IsCurrent(canonical);
ulong authorityVersion = canonical.ObjDescAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.ObjDescAuthorityVersion == authorityVersion);
}
public bool TryApplyPickup(
@ -370,11 +421,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
Entities.ParentAttachments.EndChildProjection(update.Guid);
acknowledgeProjection?.Invoke(canonical);
if (!Entities.IsCurrent(canonical))
return false;
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
return Entities.IsCurrent(canonical);
ulong positionVersion = canonical.PositionAuthorityVersion;
ulong spatialVersion = canonical.SpatialAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Withdrawn,
() => canonical.PositionAuthorityVersion == positionVersion
&& canonical.SpatialAuthorityVersion == spatialVersion);
}
public bool TryApplyCreateParent(
@ -427,11 +481,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
}
Entities.RefreshSnapshot(canonical, accepted);
acknowledgeProjection?.Invoke(canonical);
if (!Entities.IsCurrent(canonical))
return false;
PublishEntity(RuntimeEntityChange.Updated, canonical);
return Entities.IsCurrent(canonical);
Entities.AdvanceParentCommit(canonical);
ulong parentCommitVersion = canonical.ParentCommitVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.ParentCommitVersion
== parentCommitVersion);
}
public bool CommitAcceptedParentCellless(
@ -449,15 +506,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
acknowledgeProjection?.Invoke(canonical);
if (!Entities.IsCurrent(canonical)
|| canonical.PositionAuthorityVersion != positionAuthorityVersion)
{
return false;
}
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
return Entities.IsCurrent(canonical)
&& canonical.PositionAuthorityVersion == positionAuthorityVersion;
ulong spatialVersion = canonical.SpatialAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Withdrawn,
() => canonical.PositionAuthorityVersion
== positionAuthorityVersion
&& canonical.SpatialAuthorityVersion == spatialVersion);
}
public bool TryApplyMotion(
@ -483,11 +539,20 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.RefreshSnapshot(canonical, snapshot);
if (applied && retainPayload)
Entities.AdvanceMovementAuthority(canonical);
if (applied)
{
Entities.AdvanceMovementCommit(canonical);
ulong movementCommitVersion =
canonical.MovementCommitVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.MovementCommitVersion
== movementCommitVersion);
}
acknowledgeProjection?.Invoke(canonical);
if (applied && Entities.IsCurrent(canonical))
PublishEntity(RuntimeEntityChange.Updated, canonical);
if (applied && !Entities.IsCurrent(canonical))
return false;
}
return applied;
@ -510,11 +575,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.RefreshSnapshot(canonical, accepted);
Entities.AdvanceVectorAuthority(canonical);
acknowledgeProjection?.Invoke(canonical);
if (!Entities.IsCurrent(canonical))
return false;
PublishEntity(RuntimeEntityChange.Updated, canonical);
return Entities.IsCurrent(canonical);
ulong authorityVersion = canonical.VectorAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.VectorAuthorityVersion == authorityVersion);
}
public bool TryApplyState(
@ -539,16 +605,22 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
transition = Entities.ApplyRawPhysicsState(
canonical,
update.PhysicsState);
acknowledgeProjection?.Invoke(canonical, transition);
if (!Entities.IsCurrent(canonical))
return false;
PublishEntity(
transition.HiddenTransition
ulong stateVersion = canonical.StateAuthorityVersion;
ulong physicsMutationVersion =
canonical.PhysicsStateMutationVersion;
RetailPhysicsStateTransition committedTransition = transition;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(
canonical,
committedTransition),
committedTransition.HiddenTransition
is RetailHiddenTransition.BecameHidden
? RuntimeEntityChange.Hidden
: RuntimeEntityChange.Updated,
canonical);
return Entities.IsCurrent(canonical);
() => canonical.StateAuthorityVersion == stateVersion
&& canonical.PhysicsStateMutationVersion
== physicsMutationVersion);
}
public bool TryApplyPosition(
@ -611,18 +683,25 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.ParentAttachments.EndChildProjection(update.Guid);
}
acknowledgeProjection?.Invoke(canonical);
if (!Entities.IsCurrent(canonical))
return false;
if (acceptedPosition)
ulong positionVersion = canonical.PositionAuthorityVersion;
ulong spatialVersion = canonical.SpatialAuthorityVersion;
if (!acceptedPosition)
{
PublishEntity(
beforeCell != canonical.FullCellId
? RuntimeEntityChange.Rebucketed
: RuntimeEntityChange.Updated,
canonical);
acknowledgeProjection?.Invoke(canonical);
return IsExpectedCanonical(
canonical,
() => canonical.PositionAuthorityVersion == positionVersion
&& canonical.SpatialAuthorityVersion == spatialVersion);
}
return Entities.IsCurrent(canonical);
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
beforeCell != canonical.FullCellId
? RuntimeEntityChange.Rebucketed
: RuntimeEntityChange.Updated,
() => canonical.PositionAuthorityVersion == positionVersion
&& canonical.SpatialAuthorityVersion == spatialVersion);
}
public bool CommitRebucket(
@ -641,12 +720,21 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
canonical,
fullCellId,
canonicalLandblockId);
acknowledgeProjection?.Invoke(canonical);
if (!Entities.IsCurrent(canonical))
return false;
if (previous != fullCellId)
PublishEntity(RuntimeEntityChange.Rebucketed, canonical);
return Entities.IsCurrent(canonical);
ulong spatialVersion = canonical.SpatialAuthorityVersion;
if (previous == fullCellId)
{
acknowledgeProjection?.Invoke(canonical);
return IsExpectedCanonical(
canonical,
() => canonical.SpatialAuthorityVersion
== spatialVersion);
}
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Rebucketed,
() => canonical.SpatialAuthorityVersion == spatialVersion);
}
public bool CommitWithdrawal(
@ -660,11 +748,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
acknowledgeProjection?.Invoke(canonical);
if (!Entities.IsCurrent(canonical))
return false;
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
return Entities.IsCurrent(canonical);
ulong spatialVersion = canonical.SpatialAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Withdrawn,
() => canonical.SpatialAuthorityVersion == spatialVersion);
}
public bool CommitChildNoDraw(
@ -678,11 +767,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
return false;
Entities.SetChildNoDraw(canonical, noDraw);
acknowledgeProjection?.Invoke(canonical);
if (!Entities.IsCurrent(canonical))
return false;
PublishEntity(RuntimeEntityChange.Updated, canonical);
return Entities.IsCurrent(canonical);
ulong physicsMutationVersion =
canonical.PhysicsStateMutationVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.PhysicsStateMutationVersion
== physicsMutationVersion);
}
public bool RetireAfterProjectionAcquisitionFailure(
@ -694,6 +786,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
return false;
Entities.AdvanceLifetimeMutation(canonical.ServerGuid);
Entities.RetainTeardown(canonical);
PublishEntity(RuntimeEntityChange.Deleted, canonical);
return true;
}
@ -731,6 +824,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
&& Entities.RemoveActive(active))
{
retiredCanonical = active;
Entities.RetainTeardown(active);
PublishEntity(
removeRetainedObject
? RuntimeEntityChange.Deleted
@ -805,6 +899,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
{
if (!Entities.RemoveActive(canonical))
continue;
Entities.RetainTeardown(canonical);
PublishEntity(RuntimeEntityChange.Deleted, canonical);
}
return retired;
@ -827,8 +922,52 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
{
if (_disposed)
return;
_disposed = true;
Events.Dispose();
// Disposal is terminal, unlike a session reset. Quiesce borrowers
// before committing internal cleanup so no observer can re-enter a
// lifetime whose owner is being destroyed.
Events.DetachObservers();
List<Exception>? failures = null;
try
{
if (!_sessionClearInProgress)
_ = BeginSessionClear();
try
{
ClearObjects();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
foreach (RuntimeEntityRecord canonical
in Entities.TeardownRecords.ToArray())
{
Exception? failure = RetireCanonicalOnly(canonical);
if (failure is not null)
(failures ??= []).Add(failure);
}
if (!CompleteSessionClearIfConverged())
{
(failures ??= []).Add(new InvalidOperationException(
"Runtime entity/object disposal did not converge every canonical owner."));
}
}
finally
{
_disposed = true;
Events.Dispose();
}
if (failures is not null)
{
throw new AggregateException(
"Runtime entity/object disposal failed to converge.",
failures);
}
}
private bool CommitPositionChannelUpdate(
@ -847,11 +986,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.RefreshSnapshot(canonical, accepted);
Entities.AdvancePositionAuthority(canonical);
acknowledgeProjection?.Invoke(canonical);
if (!Entities.IsCurrent(canonical))
return false;
PublishEntity(RuntimeEntityChange.Updated, canonical);
return Entities.IsCurrent(canonical);
ulong positionVersion = canonical.PositionAuthorityVersion;
ulong spatialVersion = canonical.SpatialAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.PositionAuthorityVersion == positionVersion
&& canonical.SpatialAuthorityVersion == spatialVersion);
}
private void PublishEntity(
@ -859,6 +1001,37 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
RuntimeEntityRecord canonical) =>
Events.PublishEntity(change, canonical);
private bool AcknowledgeProjectionAndPublish(
RuntimeEntityRecord canonical,
Action acknowledgeProjection,
RuntimeEntityChange change,
Func<bool> matchesCommittedMutation)
{
try
{
acknowledgeProjection();
}
finally
{
if (IsExpectedCanonical(
canonical,
matchesCommittedMutation))
{
PublishEntity(change, canonical);
}
}
return IsExpectedCanonical(
canonical,
matchesCommittedMutation);
}
private bool IsExpectedCanonical(
RuntimeEntityRecord canonical,
Func<bool> matchesCommittedMutation) =>
Entities.IsCurrent(canonical)
&& matchesCommittedMutation();
private void EnsureNotDisposed() =>
ObjectDisposedException.ThrowIf(_disposed, this);

View file

@ -50,6 +50,8 @@ public sealed class RuntimeEntityRecord
public uint CanonicalLandblockId { get; internal set; }
public uint RawPhysicsState { get; internal set; }
public PhysicsStateFlags FinalPhysicsState { get; internal set; }
public ulong SpatialAuthorityVersion { get; private set; }
public ulong PhysicsStateMutationVersion { get; private set; }
/// <summary>
/// Incarnation-stable retail <c>CPhysicsObj::update_time</c> owner.
@ -72,6 +74,8 @@ public sealed class RuntimeEntityRecord
public ulong VectorAuthorityVersion { get; private set; }
public ulong VelocityAuthorityVersion { get; private set; }
public ulong MovementAuthorityVersion { get; private set; }
public ulong MovementCommitVersion { get; private set; } = 1UL;
public ulong ParentCommitVersion { get; private set; }
public ulong ObjDescAuthorityVersion { get; private set; }
public ulong CreateIntegrationVersion { get; private set; } = 1UL;
public bool DeleteAcceptedForTeardown { get; internal set; }
@ -94,6 +98,7 @@ public sealed class RuntimeEntityRecord
internal RetailPhysicsStateTransition ApplyRawPhysicsState(uint rawState)
{
StateAuthorityVersion++;
PhysicsStateMutationVersion++;
RawPhysicsState = rawState;
RetailPhysicsStateTransition transition = RetailPhysicsStateTransitions.Apply(
FinalPhysicsState,
@ -123,6 +128,10 @@ public sealed class RuntimeEntityRecord
VelocityAuthorityVersion++;
}
internal void AdvanceMovementCommit() => MovementCommitVersion++;
internal void AdvanceParentCommit() => ParentCommitVersion++;
internal void AdvanceObjDescAuthority() => ObjDescAuthorityVersion++;
internal void AdvanceCreateAuthority()
@ -138,6 +147,7 @@ public sealed class RuntimeEntityRecord
internal void SetChildNoDraw(bool noDraw)
{
PhysicsStateMutationVersion++;
FinalPhysicsState = noDraw
? FinalPhysicsState | PhysicsStateFlags.NoDraw
: FinalPhysicsState & ~PhysicsStateFlags.NoDraw;
@ -149,12 +159,22 @@ public sealed class RuntimeEntityRecord
{
if (refreshPosition && Snapshot.Position is { } position)
{
FullCellId = position.LandblockId;
CanonicalLandblockId = (FullCellId & 0xFFFF0000u) | 0xFFFFu;
SetFullCell(
position.LandblockId,
(position.LandblockId & 0xFFFF0000u) | 0xFFFFu);
}
RawPhysicsState = Snapshot.Physics?.RawState
?? Snapshot.PhysicsState
?? 0u;
}
internal void SetFullCell(
uint fullCellId,
uint canonicalLandblockId)
{
SpatialAuthorityVersion++;
FullCellId = fullCellId;
CanonicalLandblockId = canonicalLandblockId;
}
}