refactor(world): extract live appearance and parenting

Move ObjDesc, Parent, Pickup, child-ready, and retained projection recovery into LiveEntityHydrationController while preserving retail timestamp and leave-world semantics. Pin ready publication to exact projection authority and restore attached descendant chains synchronously.
This commit is contained in:
Erik 2026-07-21 18:10:07 +02:00
parent 40352d5a7a
commit fe5514967c
9 changed files with 1098 additions and 199 deletions

View file

@ -1,6 +1,8 @@
using AcDream.App.Rendering;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.World;
namespace AcDream.App.World;
@ -29,19 +31,55 @@ internal interface ILiveEntityProjectionMaterializer
void ResetSessionState();
}
internal interface ILiveEntitySpawnRelationshipSink
internal interface ILiveEntityRelationshipProjection
{
void OnSpawn(WorldSession.EntitySpawn spawn);
void OnParent(ParentEvent.Parsed update);
void OnCreateParentAccepted(CreateParentUpdate update);
ChildUnparentDisposition OnChildBecameUnparented(uint childGuid);
bool TryApplyAttachedAppearance(
LiveEntityRecord record,
ulong objDescAuthorityVersion);
}
internal interface ILiveEntityReadyPublisher
{
bool Publish(LiveEntityRecord expectedRecord);
bool Publish(LiveEntityReadyCandidate candidate);
}
internal readonly record struct LiveEntityReadyCandidate(
LiveEntityRecord Record,
ulong CreateIntegrationVersion);
ulong CreateIntegrationVersion,
ulong PositionAuthorityVersion,
ulong ProjectionMutationVersion,
ulong ObjDescAuthorityVersion,
LiveEntityProjectionKind ProjectionKind,
bool IsSpatiallyProjected,
WorldEntity? WorldEntity)
{
internal static LiveEntityReadyCandidate Capture(LiveEntityRecord record)
{
ArgumentNullException.ThrowIfNull(record);
return new(
record,
record.CreateIntegrationVersion,
record.PositionAuthorityVersion,
record.ProjectionMutationVersion,
record.ObjDescAuthorityVersion,
record.ProjectionKind,
record.IsSpatiallyProjected,
record.WorldEntity);
}
internal bool IsCurrent(LiveEntityRuntime runtime) =>
runtime.IsCurrentCreateIntegration(Record, CreateIntegrationVersion)
&& Record.PositionAuthorityVersion == PositionAuthorityVersion
&& Record.ProjectionMutationVersion == ProjectionMutationVersion
&& Record.ObjDescAuthorityVersion == ObjDescAuthorityVersion
&& Record.ProjectionKind == ProjectionKind
&& Record.IsSpatiallyProjected == IsSpatiallyProjected
&& ReferenceEquals(Record.WorldEntity, WorldEntity);
}
internal interface ILiveEntityNetworkUpdateSink
{
@ -104,7 +142,7 @@ internal sealed class LiveEntityHydrationController
private readonly ClientObjectTable _objects;
private readonly object _datLock;
private readonly ILiveEntityProjectionMaterializer _materializer;
private readonly ILiveEntitySpawnRelationshipSink _relationships;
private readonly ILiveEntityRelationshipProjection _relationships;
private readonly ILiveEntityReadyPublisher _ready;
private readonly ILiveEntityWorldOriginCoordinator _origin;
private readonly ILiveEntityNetworkUpdateSink _networkUpdates;
@ -117,7 +155,7 @@ internal sealed class LiveEntityHydrationController
ClientObjectTable objects,
object datLock,
ILiveEntityProjectionMaterializer materializer,
ILiveEntitySpawnRelationshipSink relationships,
ILiveEntityRelationshipProjection relationships,
ILiveEntityReadyPublisher ready,
ILiveEntityWorldOriginCoordinator origin,
ILiveEntityNetworkUpdateSink networkUpdates,
@ -139,6 +177,8 @@ internal sealed class LiveEntityHydrationController
_diagnostic = diagnostic;
}
internal event Action<uint>? AppearanceApplied;
public void OnCreate(WorldSession.EntitySpawn spawn)
{
// DatCollection uses one mutable reader cursor shared with streaming.
@ -228,6 +268,14 @@ internal sealed class LiveEntityHydrationController
expectedCreateIntegrationVersion:
createIntegrationVersion);
}
if (_runtime.IsCurrentRecord(record)
&& record.AppearanceProjectionSynchronizationPending)
{
SynchronizeAppearance(
record,
record.ObjDescAuthorityVersion);
}
}
}
catch (Exception applyFailure)
@ -248,6 +296,68 @@ internal sealed class LiveEntityHydrationController
}
}
/// <summary>
/// Applies retail <c>SmartBox::UpdateVisualDesc @ 0x00451F40</c> to the
/// retained object. Identity, animation owner, physics, effects, and cell
/// membership survive; only the DAT-backed visual projection and its
/// derived collision payload are replaced.
/// </summary>
public bool OnAppearance(ObjDescEvent.Parsed update)
{
lock (_datLock)
{
if (!_runtime.TryApplyObjDesc(update, out _)
|| !_runtime.TryGetRecord(update.Guid, out LiveEntityRecord record))
{
return false;
}
return SynchronizeAppearance(
record,
record.ObjDescAuthorityVersion);
}
}
/// <summary>
/// Retail <c>DoPickupEvent @ 0x00452240</c>: consume the shared Position
/// timestamp, unparent, and leave world without destroying the logical
/// object or any incarnation-scoped owner.
/// </summary>
public bool OnPickup(PickupEvent.Parsed update)
{
if (!_runtime.TryApplyPickup(update, out _))
return false;
ChildUnparentDisposition disposition =
_relationships.OnChildBecameUnparented(update.Guid);
bool accepted = disposition is not ChildUnparentDisposition.Superseded;
if (accepted)
{
_diagnostic?.Invoke(
$"live: pickup guid=0x{update.Guid:X8} instSeq={update.InstanceSequence} posSeq={update.PositionSequence}");
}
return accepted;
}
/// <summary>
/// Parent events may precede either CreateObject. The relationship owner
/// retains wire order and invokes <see cref="TryAcceptParentForProjection"/>
/// only when both exact incarnations are addressable.
/// </summary>
public void OnParent(ParentEvent.Parsed update) =>
_relationships.OnParent(update);
internal bool TryAcceptParentForProjection(ParentEvent.Parsed update) =>
_runtime.TryApplyParent(update, out _);
internal bool OnCreateParentAccepted(CreateParentUpdate update)
{
if (!_runtime.TryApplyCreateParent(update, out _))
return false;
_relationships.OnCreateParentAccepted(update);
return true;
}
/// <summary>
/// Reprojects retained live objects after their landblock is loaded. An
/// fully hydrated record only rebuckets. A retained partial projection
@ -263,7 +373,8 @@ internal sealed class LiveEntityHydrationController
LiveEntityRecord[] records = _runtime.Records
.Where(record => (record.ServerGuid != _playerGuid()
|| !record.InitialHydrationCompleted
|| record.CreateProjectionSynchronizationPending)
|| record.CreateProjectionSynchronizationPending
|| record.AppearanceProjectionSynchronizationPending)
&& record.ProjectionCellId != 0
&& ((record.ProjectionCellId & 0xFFFF0000u) | 0xFFFFu) == canonical
&& record.Snapshot.SetupTableId is not null)
@ -279,6 +390,9 @@ internal sealed class LiveEntityHydrationController
if (!_runtime.IsCurrentRecord(record))
continue;
ulong projectedObjDescVersion = record.ObjDescAuthorityVersion;
bool projectedCanonicalAppearance = false;
if (record.CreateProjectionSynchronizationPending)
{
if (ProjectExact(
@ -287,6 +401,7 @@ internal sealed class LiveEntityHydrationController
LiveProjectionPurpose.CreateSupersessionRecovery))
{
projected++;
projectedCanonicalAppearance = true;
}
}
else if (record.WorldEntity is not null
@ -303,6 +418,24 @@ internal sealed class LiveEntityHydrationController
record,
record.Snapshot,
LiveProjectionPurpose.SpatialRecovery))
{
projected++;
projectedCanonicalAppearance = true;
}
if (projectedCanonicalAppearance
&& record.AppearanceProjectionSynchronizationPending)
{
CompleteAppearanceProjectionSynchronization(
record,
projectedObjDescVersion);
}
if (_runtime.IsCurrentRecord(record)
&& record.AppearanceProjectionSynchronizationPending
&& SynchronizeAppearance(
record,
record.ObjDescAuthorityVersion))
{
projected++;
}
@ -353,7 +486,8 @@ internal sealed class LiveEntityHydrationController
return false;
}
return ProjectExact(
ulong projectedObjDescVersion = expectedRecord.ObjDescAuthorityVersion;
bool projected = ProjectExact(
expectedRecord,
acceptedSpawn,
expectedRecord.CreateProjectionSynchronizationPending
@ -362,49 +496,128 @@ internal sealed class LiveEntityHydrationController
&& _runtime.IsCurrentPositionAuthority(
expectedRecord,
positionAuthorityVersion);
}
}
if (!projected)
return false;
public bool ApplyAppearance(
LiveEntityRecord expectedRecord,
WorldSession.EntitySpawn acceptedSpawn,
LiveEntityAppearanceUpdateState? appearanceUpdate)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
lock (_datLock)
{
return ProjectExact(
if (expectedRecord.AppearanceProjectionSynchronizationPending)
{
if (_runtime.IsCurrentObjDescAuthority(
expectedRecord,
projectedObjDescVersion))
{
CompleteAppearanceProjectionSynchronization(
expectedRecord,
projectedObjDescVersion);
}
else if (!SynchronizeAppearance(
expectedRecord,
expectedRecord.ObjDescAuthorityVersion))
{
return false;
}
}
return _runtime.IsCurrentPositionAuthority(
expectedRecord,
acceptedSpawn,
appearanceUpdate is null
? LiveProjectionPurpose.SpatialRecovery
: LiveProjectionPurpose.AppearanceMutation,
appearanceUpdate);
positionAuthorityVersion);
}
}
public bool OnEntityReady(LiveEntityReadyCandidate candidate)
{
if (!PublishReady(
candidate.Record,
candidate.CreateIntegrationVersion))
if (!PublishReady(candidate))
{
return false;
}
return !candidate.Record.CreateProjectionSynchronizationPending
if (candidate.Record.AppearanceProjectionSynchronizationPending
&& !candidate.Record.AppearanceHydrationInProgress)
{
CompleteAppearanceProjectionSynchronization(
candidate.Record,
candidate.ObjDescAuthorityVersion);
}
return candidate.IsCurrent(_runtime)
&& (!candidate.Record.CreateProjectionSynchronizationPending
|| _runtime.TryCompleteCreateProjectionSynchronization(
candidate.Record,
candidate.CreateIntegrationVersion);
candidate.CreateIntegrationVersion));
}
public void ResetSessionState() => _materializer.ResetSessionState();
private bool SynchronizeAppearance(
LiveEntityRecord expectedRecord,
ulong expectedObjDescAuthorityVersion)
{
while (true)
{
if (!_runtime.TryBeginAppearanceHydration(
expectedRecord,
expectedObjDescAuthorityVersion))
{
return false;
}
bool retry;
bool published;
try
{
if (expectedRecord.ProjectionKind is
LiveEntityProjectionKind.Attached)
{
published = _relationships.TryApplyAttachedAppearance(
expectedRecord,
expectedObjDescAuthorityVersion);
}
else
{
LiveEntityAppearanceUpdateState? appearanceState =
LiveEntityAppearanceBinding.Capture(
_runtime,
expectedRecord.ServerGuid);
if (appearanceState is null)
{
published = ProjectExact(
expectedRecord,
expectedRecord.Snapshot,
LiveProjectionPurpose.SpatialRecovery);
}
else
{
published = _materializer.TryMaterialize(
expectedRecord,
expectedRecord.Snapshot,
LiveProjectionPurpose.AppearanceMutation,
expectedRecord.CreateIntegrationVersion,
appearanceState);
}
}
}
finally
{
retry = _runtime.EndAppearanceHydration(expectedRecord);
}
if (retry && _runtime.IsCurrentRecord(expectedRecord))
{
expectedObjDescAuthorityVersion =
expectedRecord.ObjDescAuthorityVersion;
continue;
}
return published
&& CompleteAppearanceProjectionSynchronization(
expectedRecord,
expectedObjDescAuthorityVersion);
}
}
private bool ProjectExact(
LiveEntityRecord expectedRecord,
WorldSession.EntitySpawn acceptedSpawn,
LiveProjectionPurpose purpose,
LiveEntityAppearanceUpdateState? appearanceUpdate = null,
ulong? expectedCreateIntegrationVersion = null)
{
while (true)
@ -433,7 +646,6 @@ internal sealed class LiveEntityHydrationController
expectedRecord,
acceptedSpawn,
purpose,
appearanceUpdate,
createIntegrationVersion);
}
finally
@ -449,7 +661,6 @@ internal sealed class LiveEntityHydrationController
// newest canonical snapshot; logical resources remain registered.
acceptedSpawn = expectedRecord.Snapshot;
purpose = LiveProjectionPurpose.CreateSupersessionRecovery;
appearanceUpdate = null;
expectedCreateIntegrationVersion =
expectedRecord.CreateIntegrationVersion;
}
@ -459,7 +670,6 @@ internal sealed class LiveEntityHydrationController
LiveEntityRecord expectedRecord,
WorldSession.EntitySpawn acceptedSpawn,
LiveProjectionPurpose purpose,
LiveEntityAppearanceUpdateState? appearanceUpdate,
ulong createIntegrationVersion)
{
_relationships.OnSpawn(acceptedSpawn);
@ -550,8 +760,7 @@ internal sealed class LiveEntityHydrationController
expectedRecord,
expectedRecord.Snapshot,
purpose,
createIntegrationVersion,
appearanceUpdate);
createIntegrationVersion);
if (!materialized
|| !_runtime.IsCurrentCreateIntegration(
expectedRecord,
@ -579,22 +788,46 @@ internal sealed class LiveEntityHydrationController
ulong expectedCreateIntegrationVersion)
{
if (!_runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion)
|| !_ready.Publish(expectedRecord)
|| !_runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion))
{
return false;
}
return PublishReady(LiveEntityReadyCandidate.Capture(expectedRecord));
}
private bool PublishReady(LiveEntityReadyCandidate candidate)
{
LiveEntityRecord expectedRecord = candidate.Record;
if (!candidate.IsCurrent(_runtime)
|| !_ready.Publish(candidate)
|| !candidate.IsCurrent(_runtime))
{
return false;
}
return _runtime.TryMarkInitialHydrationCompleted(
expectedRecord,
expectedCreateIntegrationVersion)
&& _runtime.IsCurrentCreateIntegration(
candidate.CreateIntegrationVersion)
&& candidate.IsCurrent(_runtime);
}
private bool CompleteAppearanceProjectionSynchronization(
LiveEntityRecord expectedRecord,
ulong expectedObjDescAuthorityVersion)
{
if (!_runtime.TryCompleteAppearanceProjectionSynchronization(
expectedRecord,
expectedCreateIntegrationVersion);
expectedObjDescAuthorityVersion))
{
return false;
}
AppearanceApplied?.Invoke(expectedRecord.ServerGuid);
return _runtime.IsCurrentObjDescAuthority(
expectedRecord,
expectedObjDescAuthorityVersion);
}
private void InitializeOriginAndRecoverLoaded(

View file

@ -1,16 +1,48 @@
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
namespace AcDream.App.World;
internal sealed class DelegateLiveEntitySpawnRelationshipSink(
Action<WorldSession.EntitySpawn> onSpawn) : ILiveEntitySpawnRelationshipSink
internal sealed class LiveEntityRelationshipProjection(
EquippedChildRenderController children) : ILiveEntityRelationshipProjection
{
private readonly Action<WorldSession.EntitySpawn> _onSpawn =
onSpawn ?? throw new ArgumentNullException(nameof(onSpawn));
private readonly EquippedChildRenderController _children =
children ?? throw new ArgumentNullException(nameof(children));
public void OnSpawn(WorldSession.EntitySpawn spawn) => _onSpawn(spawn);
public void OnSpawn(WorldSession.EntitySpawn spawn) => _children.OnSpawn(spawn);
public void OnParent(ParentEvent.Parsed update) => _children.OnParentEvent(update);
public void OnCreateParentAccepted(CreateParentUpdate update) =>
_children.OnCreateParentAccepted(update);
public ChildUnparentDisposition OnChildBecameUnparented(uint childGuid) =>
_children.OnChildBecameUnparented(childGuid);
public bool TryApplyAttachedAppearance(
LiveEntityRecord record,
ulong objDescAuthorityVersion) =>
_children.TryApplyAttachedAppearance(record, objDescAuthorityVersion);
}
/// <summary>
/// Fail-fast construction seam for the retail ParentEvent timestamp gate.
/// The equipped-child renderer can be built before hydration, but no live
/// session may resolve a relation until hydration binds this once.
/// </summary>
internal sealed class DeferredLiveEntityParentAcceptance
{
private Func<ParentEvent.Parsed, bool>? _accept;
public void Bind(Func<ParentEvent.Parsed, bool> accept)
{
ArgumentNullException.ThrowIfNull(accept);
if (Interlocked.CompareExchange(ref _accept, accept, null) is not null)
throw new InvalidOperationException("Live parent acceptance is already bound.");
}
public bool TryAccept(ParentEvent.Parsed update) =>
(_accept ?? throw new InvalidOperationException(
"Live parent acceptance must be bound before a session starts."))(update);
}
internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
@ -49,31 +81,23 @@ internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
?? throw new ArgumentNullException(nameof(replayEffects));
}
public bool Publish(LiveEntityRecord expectedRecord)
public bool Publish(LiveEntityReadyCandidate candidate)
{
LiveEntityRecord expectedRecord = candidate.Record;
ArgumentNullException.ThrowIfNull(expectedRecord);
ulong createIntegrationVersion = expectedRecord.CreateIntegrationVersion;
if (!_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion)
if (!candidate.IsCurrent(_runtime)
|| !_prepareEffects(expectedRecord)
|| !_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion))
|| !candidate.IsCurrent(_runtime))
return false;
if (!_presentState(expectedRecord)
|| !_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion))
|| !candidate.IsCurrent(_runtime))
{
return false;
}
return _replayEffects(expectedRecord)
&& _runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion);
&& candidate.IsCurrent(_runtime);
}
}

View file

@ -201,6 +201,7 @@ public sealed class LiveEntityRecord
? 0UL
: 1UL;
MovementAuthorityVersion = 1UL;
ObjDescAuthorityVersion = 1UL;
FinalPhysicsState = RetailPhysicsStateTransitions.ConstructorState;
ApplyRawPhysicsState(RawPhysicsState);
}
@ -258,6 +259,7 @@ public sealed class LiveEntityRecord
internal ulong VectorAuthorityVersion { get; private set; }
internal ulong VelocityAuthorityVersion { get; private set; }
internal ulong MovementAuthorityVersion { get; private set; }
internal ulong ObjDescAuthorityVersion { get; private set; }
/// <summary>
/// Advances for every accepted CreateObject affecting this incarnation,
/// including a fresher same-INSTANCE_TS retransmit. App integration uses
@ -284,6 +286,15 @@ public sealed class LiveEntityRecord
internal bool ProjectionHydrationInProgress { get; set; }
internal ulong ProjectionHydrationCreateVersion { get; set; }
internal bool ProjectionHydrationRetryRequested { get; set; }
/// <summary>
/// A renderer mesh transaction can be re-entered by a newer accepted
/// ObjDesc. Keep that independent projection obligation on the exact
/// incarnation until the newest visual description publishes.
/// </summary>
internal bool AppearanceProjectionSynchronizationPending { get; set; }
internal bool AppearanceHydrationInProgress { get; set; }
internal ulong AppearanceHydrationVersion { get; set; }
internal bool AppearanceHydrationRetryRequested { get; set; }
public LiveEntityProjectionKind ProjectionKind { get; internal set; }
internal bool RuntimeComponentsTeardownCompleted { get; set; }
internal LiveEntityTeardownPlan? RuntimeComponentTeardownPlan { get; set; }
@ -338,6 +349,14 @@ public sealed class LiveEntityRecord
VelocityAuthorityVersion++;
}
internal void AdvanceObjDescAuthority()
{
ObjDescAuthorityVersion++;
AppearanceProjectionSynchronizationPending = true;
if (AppearanceHydrationInProgress)
AppearanceHydrationRetryRequested = true;
}
internal void AdvanceCreateAuthority()
{
PositionAuthorityVersion++;
@ -345,6 +364,7 @@ public sealed class LiveEntityRecord
VectorAuthorityVersion++;
VelocityAuthorityVersion++;
MovementAuthorityVersion++;
ObjDescAuthorityVersion++;
CreateIntegrationVersion++;
}
@ -1547,7 +1567,12 @@ public sealed class LiveEntityRuntime
public bool TryApplyObjDesc(ObjDescEvent.Parsed update, out WorldSession.EntitySpawn accepted)
{
bool applied = _inbound.TryApplyObjDesc(update, out accepted);
if (applied) RefreshRecord(update.Guid, accepted);
if (applied)
{
RefreshRecord(update.Guid, accepted);
if (_recordsByGuid.TryGetValue(update.Guid, out LiveEntityRecord? record))
record.AdvanceObjDescAuthority();
}
return applied;
}
@ -1805,6 +1830,13 @@ public sealed class LiveEntityRuntime
&& ReferenceEquals(current, record)
&& current.MovementAuthorityVersion == authorityVersion;
internal bool IsCurrentObjDescAuthority(
LiveEntityRecord record,
ulong authorityVersion) =>
_recordsByGuid.TryGetValue(record.ServerGuid, out LiveEntityRecord? current)
&& ReferenceEquals(current, record)
&& current.ObjDescAuthorityVersion == authorityVersion;
/// <summary>
/// Copies the canonical loaded root-object workset used by retail
/// <c>CPhysics::UseTime</c>. The record reference is the incarnation token;
@ -2156,6 +2188,63 @@ public sealed class LiveEntityRuntime
return retry;
}
internal bool TryBeginAppearanceHydration(
LiveEntityRecord expectedRecord,
ulong expectedObjDescAuthorityVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!expectedRecord.AppearanceProjectionSynchronizationPending
|| !IsCurrentObjDescAuthority(
expectedRecord,
expectedObjDescAuthorityVersion))
{
return false;
}
expectedRecord.AppearanceProjectionSynchronizationPending = true;
if (expectedRecord.AppearanceHydrationInProgress)
{
expectedRecord.AppearanceHydrationRetryRequested = true;
return false;
}
expectedRecord.AppearanceHydrationInProgress = true;
expectedRecord.AppearanceHydrationVersion =
expectedObjDescAuthorityVersion;
expectedRecord.AppearanceHydrationRetryRequested = false;
return true;
}
internal bool EndAppearanceHydration(LiveEntityRecord expectedRecord)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
bool retry = IsCurrentRecord(expectedRecord)
&& (expectedRecord.AppearanceHydrationRetryRequested
|| expectedRecord.ObjDescAuthorityVersion
!= expectedRecord.AppearanceHydrationVersion);
expectedRecord.AppearanceHydrationInProgress = false;
expectedRecord.AppearanceHydrationVersion = 0UL;
expectedRecord.AppearanceHydrationRetryRequested = false;
return retry;
}
internal bool TryCompleteAppearanceProjectionSynchronization(
LiveEntityRecord expectedRecord,
ulong expectedObjDescAuthorityVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!expectedRecord.AppearanceProjectionSynchronizationPending
|| !IsCurrentObjDescAuthority(
expectedRecord,
expectedObjDescAuthorityVersion))
{
return false;
}
expectedRecord.AppearanceProjectionSynchronizationPending = false;
return true;
}
internal bool IsCurrentCreateIntegration(
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion) =>
@ -2722,6 +2811,10 @@ public sealed class LiveEntityRuntime
record.ProjectionHydrationInProgress = false;
record.ProjectionHydrationCreateVersion = 0UL;
record.ProjectionHydrationRetryRequested = false;
record.AppearanceProjectionSynchronizationPending = false;
record.AppearanceHydrationInProgress = false;
record.AppearanceHydrationVersion = 0UL;
record.AppearanceHydrationRetryRequested = false;
record.WorldEntity = null;
}
finally