refactor(runtime): own canonical entity identity and incarnations

Introduce the presentation-free RuntimeEntityDirectory and RuntimeEntityRecord as the sole owners of server GUIDs, INSTANCE_TS incarnations, accepted snapshots, authority versions, retryable tombstones, session epochs, and local-ID allocation. Convert LiveEntityRuntime into an App projection sidecar host resolved through canonical record identity without a second GUID map.

Preserve the existing synchronous and reentrant projection lifecycle, including retryable registration/delete/session teardown. Add Runtime-only identity/lifetime tests and App ownership guards.

Validated by the Release solution build and 8,441 complete Release tests with five existing skips.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-25 20:07:14 +02:00
parent f7442d13e9
commit f46ddb5cdb
5 changed files with 1218 additions and 225 deletions

View file

@ -209,48 +209,78 @@ public sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceLif
} }
/// <summary> /// <summary>
/// The one logical record for a server object incarnation. The record survives /// App projection sidecar for one canonical <see cref="RuntimeEntityRecord"/>.
/// loaded/pending landblock movement and temporary loss of its render bucket. /// The canonical record survives loaded/pending landblock movement and
/// Only an accepted DeleteObject, session reset, newer INSTANCE_TS, or the /// temporary loss of its render bucket; this sidecar retains only graphical
/// active side of the leave-visibility lifecycle ends it. ACE-compatible /// components, visibility, hydration, and teardown progress.
/// cold snapshot ownership lives outside this active runtime.
/// </summary> /// </summary>
public sealed class LiveEntityRecord public sealed class LiveEntityRecord
{ {
private readonly Queue<RetailPhysicsStateTransition> _pendingStateTransitions = new(); private readonly RuntimeEntityDirectory _directory;
internal LiveEntityRecord(WorldSession.EntitySpawn snapshot) internal LiveEntityRecord(
RuntimeEntityDirectory directory,
RuntimeEntityRecord canonical)
{ {
ServerGuid = snapshot.Guid; _directory = directory ?? throw new ArgumentNullException(nameof(directory));
Snapshot = snapshot; Canonical = canonical ?? throw new ArgumentNullException(nameof(canonical));
RefreshDerivedState();
PositionAuthorityVersion = snapshot.Position is null ? 0UL : 1UL;
VectorAuthorityVersion = snapshot.Physics is null ? 0UL : 1UL;
VelocityAuthorityVersion = snapshot.Position is null
&& snapshot.Physics is null
? 0UL
: 1UL;
MovementAuthorityVersion = 1UL;
ObjDescAuthorityVersion = 1UL;
FinalPhysicsState = RetailPhysicsStateTransitions.ConstructorState;
ApplyRawPhysicsState(RawPhysicsState);
} }
public uint ServerGuid { get; } /// <summary>
public ushort Generation => Snapshot.InstanceSequence; /// Detached presentation fixture used by App component tests which do not
public WorldSession.EntitySpawn Snapshot { get; internal set; } /// exercise live identity lookup. Production records are always created
/// from the session's shared RuntimeEntityDirectory.
/// </summary>
internal LiveEntityRecord(WorldSession.EntitySpawn snapshot)
: this(new RuntimeEntityDirectory(), snapshot)
{
}
private LiveEntityRecord(
RuntimeEntityDirectory directory,
WorldSession.EntitySpawn snapshot)
: this(directory, directory.AddActive(snapshot))
{
}
internal RuntimeEntityRecord Canonical { get; }
public uint ServerGuid => Canonical.ServerGuid;
public ushort Generation => Canonical.Incarnation;
public WorldSession.EntitySpawn Snapshot
{
get => Canonical.Snapshot;
internal set => _directory.RefreshSnapshot(Canonical, value);
}
public WorldEntity? WorldEntity { get; internal set; } public WorldEntity? WorldEntity { get; internal set; }
public uint? LocalEntityId => WorldEntity?.Id; public uint? LocalEntityId => Canonical.LocalEntityId ?? WorldEntity?.Id;
public uint FullCellId { get; internal set; } public uint FullCellId
public uint CanonicalLandblockId { get; internal set; } {
public uint RawPhysicsState { get; internal set; } get => Canonical.FullCellId;
public PhysicsStateFlags FinalPhysicsState { get; internal set; } internal set => _directory.SetFullCell(
Canonical,
value,
Canonical.CanonicalLandblockId);
}
public uint CanonicalLandblockId
{
get => Canonical.CanonicalLandblockId;
internal set => _directory.SetFullCell(
Canonical,
Canonical.FullCellId,
value);
}
public uint RawPhysicsState => Canonical.RawPhysicsState;
public PhysicsStateFlags FinalPhysicsState
{
get => Canonical.FinalPhysicsState;
internal set => _directory.SetFinalPhysicsState(Canonical, value);
}
/// <summary> /// <summary>
/// Incarnation-stable retail <c>CPhysicsObj::update_time</c> owner. It /// Incarnation-stable retail <c>CPhysicsObj::update_time</c> owner. It
/// survives animation and MovementManager attachment/replacement, spatial /// survives animation and MovementManager attachment/replacement, spatial
/// rebucketing, and appearance rehydration, and dies with this record. /// rebucketing, and appearance rehydration, and dies with this record.
/// </summary> /// </summary>
public RetailObjectQuantumClock ObjectClock { get; } = new(); public RetailObjectQuantumClock ObjectClock => Canonical.ObjectClock;
/// <summary> /// <summary>
/// Changes whenever this record leaves or re-enters the ordinary-object /// Changes whenever this record leaves or re-enters the ordinary-object
/// workset. Unlike <see cref="ProjectionMutationVersion"/>, loaded-to-loaded /// workset. Unlike <see cref="ProjectionMutationVersion"/>, loaded-to-loaded
@ -258,20 +288,36 @@ public sealed class LiveEntityRecord
/// catch-up batch that crossed a leave/enter boundary without rejecting an /// catch-up batch that crossed a leave/enter boundary without rejecting an
/// ordinary same-frame cell move. /// ordinary same-frame cell move.
/// </summary> /// </summary>
internal ulong ObjectClockEpoch { get; private set; } internal ulong ObjectClockEpoch => Canonical.ObjectClockEpoch;
/// <summary> /// <summary>
/// True after this incarnation has successfully constructed its retail /// True after this incarnation has successfully constructed its retail
/// <c>CPartArray</c>. This is an object-lifetime fact, not an animation- /// <c>CPartArray</c>. This is an object-lifetime fact, not an animation-
/// runtime proxy: static poses and objects without a MotionTable still /// runtime proxy: static poses and objects without a MotionTable still
/// own a PartArray and participate in the 96-unit Active gate. /// own a PartArray and participate in the 96-unit Active gate.
/// </summary> /// </summary>
public bool HasPartArray { get; internal set; } public bool HasPartArray
public PhysicsBody? PhysicsBody { get; internal set; } {
internal bool PhysicsBodyAcquisitionInProgress { get; set; } get => Canonical.HasPartArray;
internal set => _directory.SetHasPartArray(Canonical, value);
}
public PhysicsBody? PhysicsBody
{
get => Canonical.PhysicsBody;
internal set => _directory.SetPhysicsBody(Canonical, value);
}
internal bool PhysicsBodyAcquisitionInProgress
{
get => Canonical.PhysicsBodyAcquisitionInProgress;
set => _directory.SetPhysicsBodyAcquisitionInProgress(Canonical, value);
}
public ILiveEntityAnimationRuntime? AnimationRuntime { get; internal set; } public ILiveEntityAnimationRuntime? AnimationRuntime { get; internal set; }
public ILiveEntityRemoteMotionRuntime? RemoteMotionRuntime { get; internal set; } public ILiveEntityRemoteMotionRuntime? RemoteMotionRuntime { get; internal set; }
internal bool RemoteMotionBindingInProgress { get; set; } internal bool RemoteMotionBindingInProgress { get; set; }
public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost { get; internal set; } public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost
{
get => Canonical.PhysicsHost;
internal set => _directory.SetPhysicsHost(Canonical, value);
}
internal bool RequiresRemotePlacementRuntime { get; set; } internal bool RequiresRemotePlacementRuntime { get; set; }
public ILiveEntityProjectileRuntime? ProjectileRuntime { get; internal set; } public ILiveEntityProjectileRuntime? ProjectileRuntime { get; internal set; }
public ILiveEntityEffectProfile? EffectProfile { get; internal set; } public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
@ -284,18 +330,18 @@ public sealed class LiveEntityRecord
/// handler detect a newer same-incarnation packet delivered re-entrantly /// handler detect a newer same-incarnation packet delivered re-entrantly
/// while an older packet is crossing presentation or spatial callbacks. /// while an older packet is crossing presentation or spatial callbacks.
/// </summary> /// </summary>
internal ulong PositionAuthorityVersion { get; private set; } internal ulong PositionAuthorityVersion => Canonical.PositionAuthorityVersion;
internal ulong StateAuthorityVersion { get; private set; } internal ulong StateAuthorityVersion => Canonical.StateAuthorityVersion;
internal ulong VectorAuthorityVersion { get; private set; } internal ulong VectorAuthorityVersion => Canonical.VectorAuthorityVersion;
internal ulong VelocityAuthorityVersion { get; private set; } internal ulong VelocityAuthorityVersion => Canonical.VelocityAuthorityVersion;
internal ulong MovementAuthorityVersion { get; private set; } internal ulong MovementAuthorityVersion => Canonical.MovementAuthorityVersion;
internal ulong ObjDescAuthorityVersion { get; private set; } internal ulong ObjDescAuthorityVersion => Canonical.ObjDescAuthorityVersion;
/// <summary> /// <summary>
/// Advances for every accepted CreateObject affecting this incarnation, /// Advances for every accepted CreateObject affecting this incarnation,
/// including a fresher same-INSTANCE_TS retransmit. App integration uses /// including a fresher same-INSTANCE_TS retransmit. App integration uses
/// it to distinguish two transactions that intentionally share identity. /// it to distinguish two transactions that intentionally share identity.
/// </summary> /// </summary>
internal ulong CreateIntegrationVersion { get; private set; } = 1UL; internal ulong CreateIntegrationVersion => Canonical.CreateIntegrationVersion;
public bool WorldSpawnPublished { get; internal set; } public bool WorldSpawnPublished { get; internal set; }
/// <summary> /// <summary>
/// True only after the incarnation's projection, collision/animation /// True only after the incarnation's projection, collision/animation
@ -330,82 +376,45 @@ public sealed class LiveEntityRecord
internal LiveEntityTeardownPlan? RuntimeComponentTeardownPlan { get; set; } internal LiveEntityTeardownPlan? RuntimeComponentTeardownPlan { get; set; }
internal bool SpatialProjectionTeardownCompleted { get; set; } internal bool SpatialProjectionTeardownCompleted { get; set; }
internal bool TeardownInProgress { get; set; } internal bool TeardownInProgress { get; set; }
internal bool DeleteAcceptedForTeardown { get; set; } internal bool DeleteAcceptedForTeardown
{
get => Canonical.DeleteAcceptedForTeardown;
set => _directory.SetDeleteAcceptedForTeardown(Canonical, value);
}
internal bool TryDequeueStateTransition(out RetailPhysicsStateTransition transition) => internal bool TryDequeueStateTransition(out RetailPhysicsStateTransition transition) =>
_pendingStateTransitions.TryDequeue(out transition); _directory.TryDequeueStateTransition(Canonical, out transition);
internal void SuspendObjectClock() internal void SuspendObjectClock() => _directory.SuspendObjectClock(Canonical);
{
ObjectClock.Deactivate();
ObjectClockEpoch++;
}
internal void ResetObjectClockForEnterWorld(bool isStatic) internal void ResetObjectClockForEnterWorld(bool isStatic) =>
{ _directory.ResetObjectClockForEnterWorld(Canonical, isStatic);
ObjectClock.ResetForEnterWorld(isStatic);
ObjectClockEpoch++;
}
internal RetailPhysicsStateTransition ApplyRawPhysicsState(uint rawState) internal RetailPhysicsStateTransition ApplyRawPhysicsState(uint rawState) =>
{ _directory.ApplyRawPhysicsState(Canonical, rawState);
StateAuthorityVersion++;
RawPhysicsState = rawState;
RetailPhysicsStateTransition transition = RetailPhysicsStateTransitions.Apply(
FinalPhysicsState,
(PhysicsStateFlags)rawState);
FinalPhysicsState = transition.FinalState;
if (PhysicsBody is not null)
PhysicsBody.State = FinalPhysicsState;
_pendingStateTransitions.Enqueue(transition);
return transition;
}
internal void AdvancePositionAuthority() internal void AdvancePositionAuthority() =>
{ _directory.AdvancePositionAuthority(Canonical);
PositionAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvanceVectorAuthority() internal void AdvanceVectorAuthority() =>
{ _directory.AdvanceVectorAuthority(Canonical);
VectorAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvanceMovementAuthority() internal void AdvanceMovementAuthority() =>
{ _directory.AdvanceMovementAuthority(Canonical);
MovementAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvanceObjDescAuthority() internal void AdvanceObjDescAuthority()
{ {
ObjDescAuthorityVersion++; _directory.AdvanceObjDescAuthority(Canonical);
AppearanceProjectionSynchronizationPending = true; AppearanceProjectionSynchronizationPending = true;
if (AppearanceHydrationInProgress) if (AppearanceHydrationInProgress)
AppearanceHydrationRetryRequested = true; AppearanceHydrationRetryRequested = true;
} }
internal void AdvanceCreateAuthority() internal void AdvanceCreateAuthority() =>
{ _directory.AdvanceCreateAuthority(Canonical);
PositionAuthorityVersion++;
StateAuthorityVersion++;
VectorAuthorityVersion++;
VelocityAuthorityVersion++;
MovementAuthorityVersion++;
ObjDescAuthorityVersion++;
CreateIntegrationVersion++;
}
internal void SetChildNoDraw(bool noDraw) internal void SetChildNoDraw(bool noDraw) =>
{ _directory.SetChildNoDraw(Canonical, noDraw);
FinalPhysicsState = noDraw
? FinalPhysicsState | PhysicsStateFlags.NoDraw
: FinalPhysicsState & ~PhysicsStateFlags.NoDraw;
if (PhysicsBody is not null)
PhysicsBody.State = FinalPhysicsState;
}
/// <summary> /// <summary>
/// Cell used when a streamed landblock asks live objects to restore their /// Cell used when a streamed landblock asks live objects to restore their
@ -416,17 +425,8 @@ public sealed class LiveEntityRecord
? FullCellId ? FullCellId
: Snapshot.Position?.LandblockId ?? FullCellId; : Snapshot.Position?.LandblockId ?? FullCellId;
internal void RefreshDerivedState(bool refreshPosition = true) internal void RefreshDerivedState(bool refreshPosition = true) =>
{ _directory.RefreshSnapshot(Canonical, Snapshot, refreshPosition);
if (refreshPosition && Snapshot.Position is { } position)
{
FullCellId = position.LandblockId;
CanonicalLandblockId = (FullCellId & 0xFFFF0000u) | 0xFFFFu;
}
RawPhysicsState = Snapshot.Physics?.RawState
?? Snapshot.PhysicsState
?? 0u;
}
} }
public readonly record struct LiveEntityRegistrationResult( public readonly record struct LiveEntityRegistrationResult(
@ -437,10 +437,13 @@ public readonly record struct LiveEntityRegistrationResult(
Exception? PriorGenerationCleanupFailure = null); Exception? PriorGenerationCleanupFailure = null);
/// <summary> /// <summary>
/// Update-thread owner of live server-object identity, accepted state, runtime /// Update-thread App projection/lifecycle host over the canonical
/// components, and logical teardown. <see cref="GpuWorldState"/> is a spatial /// <see cref="RuntimeEntityDirectory"/>. Runtime owns server GUIDs,
/// projection only; moving between loaded and pending buckets never replays a /// incarnations, accepted snapshots, timestamps, tombstones, and local IDs;
/// create-time renderer, script, animation, or effect action. /// this host owns graphical components and retryable projection teardown.
/// <see cref="GpuWorldState"/> is a spatial projection only; moving between
/// loaded and pending buckets never replays a create-time renderer, script,
/// animation, or effect action.
/// ///
/// <para> /// <para>
/// Retail keeps one object per accepted INSTANCE_TS: same-generation /// Retail keeps one object per accepted INSTANCE_TS: same-generation
@ -454,23 +457,31 @@ public readonly record struct LiveEntityRegistrationResult(
/// </summary> /// </summary>
public sealed class LiveEntityRuntime public sealed class LiveEntityRuntime
{ {
public const uint FirstLiveEntityId = 1_000_000u; public const uint FirstLiveEntityId = RuntimeEntityDirectory.FirstLocalEntityId;
public const uint LastLiveEntityId = 0x3FFF_FFFFu; public const uint LastLiveEntityId = RuntimeEntityDirectory.LastLocalEntityId;
private readonly GpuWorldState _spatial; private readonly GpuWorldState _spatial;
private readonly ILiveEntityResourceLifecycle _resources; private readonly ILiveEntityResourceLifecycle _resources;
private readonly ILiveEntityRuntimeComponentLifecycle _runtimeComponentLifecycle; private readonly ILiveEntityRuntimeComponentLifecycle _runtimeComponentLifecycle;
private readonly InboundPhysicsStateController _inbound = new(); private readonly RuntimeEntityDirectory _directory;
private readonly Dictionary<uint, LiveEntityRecord> _recordsByGuid = new(); // App sidecars are keyed by the canonical Runtime record itself. Runtime
// owns the only server-GUID/incarnation maps; these collections only
// attach presentation state to records already accepted by that owner.
private readonly Dictionary<RuntimeEntityRecord, LiveEntityRecord> _activeRecords = new();
// Transitional dictionary-shaped adapter for the existing App projection
// methods. It owns no GUID map: every lookup and mutation is resolved
// through RuntimeEntityDirectory, then translated to the App sidecar by
// canonical record reference. J3.3 removes this facade with the projection
// store cutover.
private readonly ActiveRecordView _recordsByGuid;
// Records leave the active gameplay map before arbitrary teardown callbacks // Records leave the active gameplay map before arbitrary teardown callbacks
// so a newer GUID generation can be accepted re-entrantly. They remain // so a newer GUID generation can be accepted re-entrantly. They remain
// canonical teardown tombstones until every resource owner confirms // canonical teardown tombstones until every resource owner confirms
// unregister; failures are therefore retryable by Delete or session Clear. // unregister; failures are therefore retryable by Delete or session Clear.
private readonly Dictionary<(uint Guid, ushort Generation), LiveEntityRecord> private readonly Dictionary<RuntimeEntityRecord, LiveEntityRecord>
_teardownRecords = new(); _teardownRecords = new();
private readonly Dictionary<uint, WorldEntity> _materializedWorldEntitiesByGuid = new(); private readonly Dictionary<uint, WorldEntity> _materializedWorldEntitiesByGuid = new();
private readonly Dictionary<uint, WorldEntity> _visibleWorldEntitiesByGuid = new(); private readonly Dictionary<uint, WorldEntity> _visibleWorldEntitiesByGuid = new();
private readonly Dictionary<uint, uint> _guidByLocalId = new();
private readonly Dictionary<uint, ILiveEntityAnimationRuntime> _animationsByLocalId = new(); private readonly Dictionary<uint, ILiveEntityAnimationRuntime> _animationsByLocalId = new();
private readonly Dictionary<uint, ILiveEntityRemoteMotionRuntime> _remoteMotionByGuid = new(); private readonly Dictionary<uint, ILiveEntityRemoteMotionRuntime> _remoteMotionByGuid = new();
// Logical components survive retail's 25-second leave-visibility lifetime. // Logical components survive retail's 25-second leave-visibility lifetime.
@ -489,10 +500,7 @@ public sealed class LiveEntityRuntime
private bool _sessionClearPendingFinalization; private bool _sessionClearPendingFinalization;
private bool _isRegisteringResources; private bool _isRegisteringResources;
private int _logicalTeardownDepth; private int _logicalTeardownDepth;
private ulong _sessionLifetimeVersion;
private readonly Dictionary<uint, ulong> _lifetimeMutationVersionByGuid = new();
private uint _rebucketingGuid; private uint _rebucketingGuid;
private uint _nextLocalEntityId;
public LiveEntityRuntime( public LiveEntityRuntime(
GpuWorldState spatial, GpuWorldState spatial,
@ -529,18 +537,15 @@ public sealed class LiveEntityRuntime
_resources = resources ?? throw new ArgumentNullException(nameof(resources)); _resources = resources ?? throw new ArgumentNullException(nameof(resources));
_runtimeComponentLifecycle = runtimeComponentLifecycle _runtimeComponentLifecycle = runtimeComponentLifecycle
?? throw new ArgumentNullException(nameof(runtimeComponentLifecycle)); ?? throw new ArgumentNullException(nameof(runtimeComponentLifecycle));
if (firstLocalEntityId is < FirstLiveEntityId or > LastLiveEntityId) _directory = new RuntimeEntityDirectory(firstLocalEntityId);
throw new ArgumentOutOfRangeException( _recordsByGuid = new ActiveRecordView(_directory, _activeRecords);
nameof(firstLocalEntityId),
$"Live entity ids must stay in 0x{FirstLiveEntityId:X8}..0x{LastLiveEntityId:X8}.");
_nextLocalEntityId = firstLocalEntityId;
_spatial.LiveProjectionVisibilityChanged += OnSpatialVisibilityChanged; _spatial.LiveProjectionVisibilityChanged += OnSpatialVisibilityChanged;
} }
public int Count => _recordsByGuid.Count; public int Count => _directory.Count;
public int PendingTeardownCount => _teardownRecords.Count; public int PendingTeardownCount => _directory.PendingTeardownCount;
public int MaterializedCount => _guidByLocalId.Count; public int MaterializedCount => _directory.ClaimedLocalIdCount;
public IReadOnlyCollection<LiveEntityRecord> Records => _recordsByGuid.Values; public IReadOnlyCollection<LiveEntityRecord> Records => _activeRecords.Values;
/// <summary> /// <summary>
/// Every materialized top-level world projection, including an object /// Every materialized top-level world projection, including an object
/// parked in a pending (currently unloaded) spatial bucket. Network and /// parked in a pending (currently unloaded) spatial bucket. Network and
@ -558,7 +563,7 @@ public sealed class LiveEntityRuntime
/// </summary> /// </summary>
public IReadOnlyDictionary<uint, WorldEntity> WorldEntities => public IReadOnlyDictionary<uint, WorldEntity> WorldEntities =>
_visibleWorldEntitiesByGuid; _visibleWorldEntitiesByGuid;
public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _inbound.Snapshots; public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _directory.Snapshots;
public IReadOnlyDictionary<uint, ILiveEntityAnimationRuntime> AnimationRuntimes => _animationsByLocalId; public IReadOnlyDictionary<uint, ILiveEntityAnimationRuntime> AnimationRuntimes => _animationsByLocalId;
public IReadOnlyDictionary<uint, ILiveEntityRemoteMotionRuntime> RemoteMotionRuntimes => _remoteMotionByGuid; public IReadOnlyDictionary<uint, ILiveEntityRemoteMotionRuntime> RemoteMotionRuntimes => _remoteMotionByGuid;
public IReadOnlyDictionary<uint, ILiveEntityAnimationRuntime> SpatialAnimationRuntimes => public IReadOnlyDictionary<uint, ILiveEntityAnimationRuntime> SpatialAnimationRuntimes =>
@ -569,7 +574,7 @@ public sealed class LiveEntityRuntime
_spatialProjectilesByGuid; _spatialProjectilesByGuid;
internal IReadOnlyDictionary<uint, LiveEntityRecord> SpatialRootObjects => internal IReadOnlyDictionary<uint, LiveEntityRecord> SpatialRootObjects =>
_spatialRootObjectsByGuid; _spatialRootObjectsByGuid;
public ParentAttachmentState ParentAttachments { get; } = new(); public ParentAttachmentState ParentAttachments => _directory.ParentAttachments;
/// <summary> /// <summary>
/// Raised after a materialized projection enters or leaves the currently /// Raised after a materialized projection enters or leaves the currently
@ -587,10 +592,10 @@ public sealed class LiveEntityRuntime
: "A live entity cannot register from inside atomic resource registration."); : "A live entity cannot register from inside atomic resource registration.");
} }
InboundCreateResult result = _inbound.AcceptCreate(incoming); InboundCreateResult result = _directory.AcceptCreate(incoming);
if (result.Disposition is CreateObjectTimestampDisposition.StaleGeneration) if (result.Disposition is CreateObjectTimestampDisposition.StaleGeneration)
return new LiveEntityRegistrationResult(result, null, false, false); return new LiveEntityRegistrationResult(result, null, false, false);
ulong sessionVersion = _sessionLifetimeVersion; ulong sessionVersion = _directory.SessionLifetimeVersion;
ulong operationVersion = AdvanceLifetimeMutation(incoming.Guid); ulong operationVersion = AdvanceLifetimeMutation(incoming.Guid);
if (result.Disposition is CreateObjectTimestampDisposition.ExistingGeneration) if (result.Disposition is CreateObjectTimestampDisposition.ExistingGeneration)
@ -608,8 +613,10 @@ public sealed class LiveEntityRuntime
// as a teardown tombstone. Do not create a second active record // as a teardown tombstone. Do not create a second active record
// with the same (GUID, INSTANCE_TS) while its partial resources are // with the same (GUID, INSTANCE_TS) while its partial resources are
// still converging; a later retransmit can recover after teardown. // still converging; a later retransmit can recover after teardown.
if (_teardownRecords.ContainsKey( if (_directory.TryGetTeardown(
(incoming.Guid, result.Snapshot.InstanceSequence))) incoming.Guid,
result.Snapshot.InstanceSequence,
out _))
{ {
return new LiveEntityRegistrationResult( return new LiveEntityRegistrationResult(
result, result,
@ -620,7 +627,9 @@ public sealed class LiveEntityRuntime
// Defensive repair for a caller that cleared only the logical map. // Defensive repair for a caller that cleared only the logical map.
// Normal session teardown clears both owners together. // Normal session teardown clears both owners together.
var recovered = new LiveEntityRecord(result.Snapshot); RuntimeEntityRecord recoveredCanonical =
_directory.AddActive(result.Snapshot);
var recovered = new LiveEntityRecord(_directory, recoveredCanonical);
_recordsByGuid.Add(incoming.Guid, recovered); _recordsByGuid.Add(incoming.Guid, recovered);
return new LiveEntityRegistrationResult(result, recovered, true, false); return new LiveEntityRegistrationResult(result, recovered, true, false);
} }
@ -657,7 +666,7 @@ public sealed class LiveEntityRuntime
// outer packet is then superseded even when the nested action left no // outer packet is then superseded even when the nested action left no
// record/snapshot (delete/reset); reinstalling it would resurrect an // record/snapshot (delete/reset); reinstalling it would resurrect an
// incarnation after an accepted terminal event. // incarnation after an accepted terminal event.
if (_sessionLifetimeVersion != sessionVersion if (_directory.SessionLifetimeVersion != sessionVersion
|| CurrentLifetimeMutation(incoming.Guid) != operationVersion) || CurrentLifetimeMutation(incoming.Guid) != operationVersion)
{ {
if (tearDownFailure is not null) if (tearDownFailure is not null)
@ -675,7 +684,8 @@ public sealed class LiveEntityRuntime
tearDownFailure); tearDownFailure);
} }
var record = new LiveEntityRecord(result.Snapshot); RuntimeEntityRecord canonical = _directory.AddActive(result.Snapshot);
var record = new LiveEntityRecord(_directory, canonical);
_recordsByGuid.Add(incoming.Guid, record); _recordsByGuid.Add(incoming.Guid, record);
return new LiveEntityRegistrationResult( return new LiveEntityRegistrationResult(
result, result,
@ -709,17 +719,30 @@ public sealed class LiveEntityRuntime
if (record.WorldEntity is null) if (record.WorldEntity is null)
{ {
uint localId = ReserveLocalEntityId(); uint localId = _directory.ClaimLocalId(record.Canonical);
WorldEntity entity = factory(localId) WorldEntity entity;
?? throw new InvalidOperationException("A live-entity projection factory returned null."); try
if (entity.Id != localId) {
throw new InvalidOperationException( entity = factory(localId)
$"Live projection id 0x{entity.Id:X8} did not match reserved id 0x{localId:X8}."); ?? throw new InvalidOperationException(
if (entity.ServerGuid != serverGuid) "A live-entity projection factory returned null.");
throw new InvalidOperationException( if (entity.Id != localId)
$"Live projection guid 0x{entity.ServerGuid:X8} did not match record 0x{serverGuid:X8}."); {
throw new InvalidOperationException(
$"Live projection id 0x{entity.Id:X8} did not match reserved id 0x{localId:X8}.");
}
if (entity.ServerGuid != serverGuid)
{
throw new InvalidOperationException(
$"Live projection guid 0x{entity.ServerGuid:X8} did not match record 0x{serverGuid:X8}.");
}
}
catch
{
_directory.ReleaseLocalId(record.Canonical);
throw;
}
_guidByLocalId.Add(localId, serverGuid);
record.WorldEntity = entity; record.WorldEntity = entity;
_isRegisteringResources = true; _isRegisteringResources = true;
try try
@ -749,7 +772,7 @@ public sealed class LiveEntityRuntime
if (rollbackError is null) if (rollbackError is null)
{ {
_guidByLocalId.Remove(localId); _directory.ReleaseLocalId(record.Canonical);
record.WorldEntity = null; record.WorldEntity = null;
throw; throw;
} }
@ -1067,14 +1090,21 @@ public sealed class LiveEntityRuntime
"A live entity cannot unregister from inside atomic resource registration."); "A live entity cannot unregister from inside atomic resource registration.");
} }
var teardownKey = (delete.Guid, delete.InstanceSequence); var teardownKey = (delete.Guid, delete.InstanceSequence);
_teardownRecords.TryGetValue(teardownKey, out LiveEntityRecord? record); LiveEntityRecord? record = null;
if (_directory.TryGetTeardown(
teardownKey.Guid,
teardownKey.InstanceSequence,
out RuntimeEntityRecord retainedCanonical))
{
_teardownRecords.TryGetValue(retainedCanonical, out record);
}
bool retryingAcceptedDelete = record is not null bool retryingAcceptedDelete = record is not null
&& (record.DeleteAcceptedForTeardown && (record.DeleteAcceptedForTeardown
|| _isClearing || _isClearing
|| _sessionClearPendingFinalization); || _sessionClearPendingFinalization);
if (!retryingAcceptedDelete) if (!retryingAcceptedDelete)
{ {
if (!_inbound.TryDelete(delete, isLocalPlayer)) if (!_directory.TryDelete(delete, isLocalPlayer))
return false; return false;
AdvanceLifetimeMutation(delete.Guid); AdvanceLifetimeMutation(delete.Guid);
ParentAttachments.DeleteGeneration(delete.Guid, delete.InstanceSequence); ParentAttachments.DeleteGeneration(delete.Guid, delete.InstanceSequence);
@ -1226,8 +1256,19 @@ public sealed class LiveEntityRuntime
_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record) _recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& record.WorldEntity is not null; && record.WorldEntity is not null;
public bool TryGetServerGuid(uint localEntityId, out uint serverGuid) => public bool TryGetServerGuid(uint localEntityId, out uint serverGuid)
_guidByLocalId.TryGetValue(localEntityId, out serverGuid); {
if (_directory.TryGetByLocalId(
localEntityId,
out RuntimeEntityRecord canonical))
{
serverGuid = canonical.ServerGuid;
return true;
}
serverGuid = 0u;
return false;
}
public bool TryGetLocalEntityId(uint serverGuid, out uint localEntityId) public bool TryGetLocalEntityId(uint serverGuid, out uint localEntityId)
{ {
@ -1377,7 +1418,7 @@ public sealed class LiveEntityRuntime
// exact record, authority epoch, and expected owners are revalidated // exact record, authority epoch, and expected owners are revalidated
// before commit just like resource/physics-body acquisition. // before commit just like resource/physics-body acquisition.
LiveEntityRecord incarnation = record; LiveEntityRecord incarnation = record;
ulong sessionVersion = _sessionLifetimeVersion; ulong sessionVersion = _directory.SessionLifetimeVersion;
ulong lifetimeVersion = CurrentLifetimeMutation(serverGuid); ulong lifetimeVersion = CurrentLifetimeMutation(serverGuid);
PhysicsBody? expectedBody = record.PhysicsBody; PhysicsBody? expectedBody = record.PhysicsBody;
ILiveEntityRemoteMotionRuntime? expectedRuntime = record.RemoteMotionRuntime; ILiveEntityRemoteMotionRuntime? expectedRuntime = record.RemoteMotionRuntime;
@ -1436,7 +1477,7 @@ public sealed class LiveEntityRuntime
== expectedIndexPresent == expectedIndexPresent
&& (!expectedIndexPresent && (!expectedIndexPresent
|| ReferenceEquals(currentIndexedRuntime, expectedIndexedRuntime)); || ReferenceEquals(currentIndexedRuntime, expectedIndexedRuntime));
if (_sessionLifetimeVersion != sessionVersion if (_directory.SessionLifetimeVersion != sessionVersion
|| CurrentLifetimeMutation(serverGuid) != lifetimeVersion || CurrentLifetimeMutation(serverGuid) != lifetimeVersion
|| !_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? current) || !_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? current)
|| !ReferenceEquals(current, incarnation) || !ReferenceEquals(current, incarnation)
@ -1592,11 +1633,11 @@ public sealed class LiveEntityRuntime
} }
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) => public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
_inbound.TryGetSnapshot(guid, out spawn); _directory.TryGetSnapshot(guid, out spawn);
public bool TryApplyObjDesc(ObjDescEvent.Parsed update, out WorldSession.EntitySpawn accepted) public bool TryApplyObjDesc(ObjDescEvent.Parsed update, out WorldSession.EntitySpawn accepted)
{ {
bool applied = _inbound.TryApplyObjDesc(update, out accepted); bool applied = _directory.TryApplyObjDesc(update, out accepted);
if (applied) if (applied)
{ {
RefreshRecord(update.Guid, accepted); RefreshRecord(update.Guid, accepted);
@ -1608,7 +1649,7 @@ public sealed class LiveEntityRuntime
public bool TryApplyPickup(PickupEvent.Parsed update, out WorldSession.EntitySpawn accepted) public bool TryApplyPickup(PickupEvent.Parsed update, out WorldSession.EntitySpawn accepted)
{ {
bool applied = _inbound.TryApplyPickup(update, out accepted); bool applied = _directory.TryApplyPickup(update, out accepted);
if (applied) if (applied)
{ {
RefreshRecord(update.Guid, accepted); RefreshRecord(update.Guid, accepted);
@ -1622,7 +1663,7 @@ public sealed class LiveEntityRuntime
public bool TryApplyCreateParent(CreateParentUpdate update, out WorldSession.EntitySpawn accepted) public bool TryApplyCreateParent(CreateParentUpdate update, out WorldSession.EntitySpawn accepted)
{ {
bool applied = _inbound.TryApplyCreateParent(update, out accepted); bool applied = _directory.TryApplyCreateParent(update, out accepted);
if (applied) if (applied)
{ {
RefreshRecord(update.ChildGuid, accepted); RefreshRecord(update.ChildGuid, accepted);
@ -1634,7 +1675,7 @@ public sealed class LiveEntityRuntime
public bool TryApplyParent(ParentEvent.Parsed update, out WorldSession.EntitySpawn accepted) public bool TryApplyParent(ParentEvent.Parsed update, out WorldSession.EntitySpawn accepted)
{ {
bool applied = _inbound.TryApplyParent(update, out accepted); bool applied = _directory.TryApplyParent(update, out accepted);
if (applied) if (applied)
{ {
RefreshRecord(update.ChildGuid, accepted); RefreshRecord(update.ChildGuid, accepted);
@ -1648,7 +1689,7 @@ public sealed class LiveEntityRuntime
ParentAttachmentRelation relation, ParentAttachmentRelation relation,
out WorldSession.EntitySpawn accepted) out WorldSession.EntitySpawn accepted)
{ {
bool committed = _inbound.TryCommitParent( bool committed = _directory.TryCommitParent(
relation.ChildGuid, relation.ChildGuid,
relation.ParentGuid, relation.ParentGuid,
relation.ParentLocation, relation.ParentLocation,
@ -1683,8 +1724,8 @@ public sealed class LiveEntityRuntime
out WorldSession.EntitySpawn accepted, out WorldSession.EntitySpawn accepted,
out AcceptedPhysicsTimestamps timestamps) out AcceptedPhysicsTimestamps timestamps)
{ {
bool applied = _inbound.TryApplyMotion(update, retainPayload, out accepted, out timestamps); bool applied = _directory.TryApplyMotion(update, retainPayload, out accepted, out timestamps);
if (_inbound.TryGetSnapshot(update.Guid, out WorldSession.EntitySpawn snapshot)) if (_directory.TryGetSnapshot(update.Guid, out WorldSession.EntitySpawn snapshot))
RefreshRecord(update.Guid, snapshot); RefreshRecord(update.Guid, snapshot);
if (applied if (applied
&& retainPayload && retainPayload
@ -1697,7 +1738,7 @@ public sealed class LiveEntityRuntime
public bool TryApplyVector(VectorUpdate.Parsed update, out WorldSession.EntitySpawn accepted) public bool TryApplyVector(VectorUpdate.Parsed update, out WorldSession.EntitySpawn accepted)
{ {
bool applied = _inbound.TryApplyVector(update, out accepted); bool applied = _directory.TryApplyVector(update, out accepted);
if (applied) if (applied)
{ {
RefreshRecord(update.Guid, accepted); RefreshRecord(update.Guid, accepted);
@ -1715,7 +1756,7 @@ public sealed class LiveEntityRuntime
out WorldSession.EntitySpawn accepted, out WorldSession.EntitySpawn accepted,
out RetailPhysicsStateTransition transition) out RetailPhysicsStateTransition transition)
{ {
bool applied = _inbound.TryApplyState(update, out accepted); bool applied = _directory.TryApplyState(update, out accepted);
transition = default; transition = default;
if (applied) if (applied)
{ {
@ -1757,7 +1798,7 @@ public sealed class LiveEntityRuntime
&& (beforePosition.FullCellId == 0 && (beforePosition.FullCellId == 0
|| !beforePosition.IsSpatiallyProjected || !beforePosition.IsSpatiallyProjected
|| !beforePosition.IsSpatiallyVisible); || !beforePosition.IsSpatiallyVisible);
bool known = _inbound.TryApplyPosition( bool known = _directory.TryApplyPosition(
update, update,
isLocalPlayer, isLocalPlayer,
forcePositionRotation, forcePositionRotation,
@ -1765,7 +1806,7 @@ public sealed class LiveEntityRuntime
out disposition, out disposition,
out accepted, out accepted,
out timestamps); out timestamps);
if (known && _inbound.TryGetSnapshot(update.Guid, out WorldSession.EntitySpawn snapshot)) if (known && _directory.TryGetSnapshot(update.Guid, out WorldSession.EntitySpawn snapshot))
{ {
bool acceptedPosition = disposition is not PositionTimestampDisposition.Rejected; bool acceptedPosition = disposition is not PositionTimestampDisposition.Rejected;
if (disposition is PositionTimestampDisposition.Apply) if (disposition is PositionTimestampDisposition.Apply)
@ -1792,7 +1833,7 @@ public sealed class LiveEntityRuntime
} }
public bool IsFreshTeleportStart(uint localPlayerGuid, ushort teleportSequence) => public bool IsFreshTeleportStart(uint localPlayerGuid, ushort teleportSequence) =>
_inbound.IsFreshTeleportStart(localPlayerGuid, teleportSequence); _directory.IsFreshTeleportStart(localPlayerGuid, teleportSequence);
/// <summary> /// <summary>
/// Retail <c>CPhysicsObj::update_object</c> (<c>0x00515D40</c>) runs root /// Retail <c>CPhysicsObj::update_object</c> (<c>0x00515D40</c>) runs root
@ -2305,8 +2346,7 @@ public sealed class LiveEntityRuntime
_isClearing = true; _isClearing = true;
_sessionClearPendingFinalization = true; _sessionClearPendingFinalization = true;
_sessionLifetimeVersion++; _directory.BeginSessionClear();
_lifetimeMutationVersionByGuid.Clear();
List<Exception>? failures = null; List<Exception>? failures = null;
try try
{ {
@ -2319,11 +2359,16 @@ public sealed class LiveEntityRuntime
RetainTeardownRecord(record); RetainTeardownRecord(record);
} }
ParentAttachments.Clear();
_inbound.Clear();
foreach (LiveEntityRecord record in _teardownRecords.Values.ToArray()) foreach (LiveEntityRecord record in _teardownRecords.Values.ToArray())
{ {
if (!_teardownRecords.TryGetValue(
record.Canonical,
out LiveEntityRecord? retained)
|| !ReferenceEquals(retained, record))
{
continue;
}
// A re-entrant Clear can observe the tombstone currently being // A re-entrant Clear can observe the tombstone currently being
// unregistered by its caller. Leave it queued; the outer // unregistered by its caller. Leave it queued; the outer
// teardown's ReleaseTeardownRecord finalizes the session once // teardown's ReleaseTeardownRecord finalizes the session once
@ -2369,6 +2414,14 @@ public sealed class LiveEntityRuntime
List<Exception>? failures = null; List<Exception>? failures = null;
foreach (LiveEntityRecord record in _teardownRecords.Values.ToArray()) foreach (LiveEntityRecord record in _teardownRecords.Values.ToArray())
{ {
if (!_teardownRecords.TryGetValue(
record.Canonical,
out LiveEntityRecord? retained)
|| !ReferenceEquals(retained, record))
{
continue;
}
if (record.TeardownInProgress) if (record.TeardownInProgress)
continue; continue;
@ -2382,7 +2435,7 @@ public sealed class LiveEntityRuntime
completed++; completed++;
if (!_recordsByGuid.ContainsKey(record.ServerGuid) if (!_recordsByGuid.ContainsKey(record.ServerGuid)
&& !HasPendingTeardown(record.ServerGuid) && !HasPendingTeardown(record.ServerGuid)
&& !_inbound.TryGetSnapshot(record.ServerGuid, out _)) && !_directory.TryGetSnapshot(record.ServerGuid, out _))
{ {
_spatial.ForgetLiveEntity(record.ServerGuid); _spatial.ForgetLiveEntity(record.ServerGuid);
} }
@ -2421,14 +2474,10 @@ public sealed class LiveEntityRuntime
default); default);
private ulong AdvanceLifetimeMutation(uint serverGuid) private ulong AdvanceLifetimeMutation(uint serverGuid)
{ => _directory.AdvanceLifetimeMutation(serverGuid);
ulong next = _lifetimeMutationVersionByGuid.GetValueOrDefault(serverGuid) + 1UL;
_lifetimeMutationVersionByGuid[serverGuid] = next;
return next;
}
private ulong CurrentLifetimeMutation(uint serverGuid) => private ulong CurrentLifetimeMutation(uint serverGuid) =>
_lifetimeMutationVersionByGuid.GetValueOrDefault(serverGuid); _directory.CurrentLifetimeMutation(serverGuid);
private bool IsCurrentProjectionOperation( private bool IsCurrentProjectionOperation(
uint serverGuid, uint serverGuid,
@ -2574,23 +2623,6 @@ public sealed class LiveEntityRuntime
} }
} }
private uint ReserveLocalEntityId()
{
uint start = _nextLocalEntityId;
do
{
uint candidate = _nextLocalEntityId;
_nextLocalEntityId = candidate == LastLiveEntityId
? FirstLiveEntityId
: candidate + 1u;
if (!_guidByLocalId.ContainsKey(candidate))
return candidate;
}
while (_nextLocalEntityId != start);
throw new InvalidOperationException("The live entity id namespace is exhausted.");
}
private void OnSpatialVisibilityChanged(uint serverGuid, bool visible) private void OnSpatialVisibilityChanged(uint serverGuid, bool visible)
{ {
// GpuWorldState serializes re-entrant visibility callbacks. A callback // GpuWorldState serializes re-entrant visibility callbacks. A callback
@ -2688,19 +2720,18 @@ public sealed class LiveEntityRuntime
_visibleWorldEntitiesByGuid.Remove(record.ServerGuid); _visibleWorldEntitiesByGuid.Remove(record.ServerGuid);
} }
private static (uint Guid, ushort Generation) TeardownKey(LiveEntityRecord record) =>
(record.ServerGuid, record.Generation);
private void RetainTeardownRecord(LiveEntityRecord record) private void RetainTeardownRecord(LiveEntityRecord record)
{ {
var key = TeardownKey(record); _directory.RetainTeardown(record.Canonical);
if (_teardownRecords.TryGetValue(key, out LiveEntityRecord? retained) if (_teardownRecords.TryGetValue(
record.Canonical,
out LiveEntityRecord? retained)
&& !ReferenceEquals(retained, record)) && !ReferenceEquals(retained, record))
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"Live entity teardown tombstone collision for 0x{record.ServerGuid:X8} generation {record.Generation}."); $"Live entity teardown tombstone collision for 0x{record.ServerGuid:X8} generation {record.Generation}.");
} }
_teardownRecords[key] = record; _teardownRecords[record.Canonical] = record;
if (record.WorldEntity is { } entity if (record.WorldEntity is { } entity
&& _visibleWorldEntitiesByGuid.TryGetValue( && _visibleWorldEntitiesByGuid.TryGetValue(
record.ServerGuid, record.ServerGuid,
@ -2713,45 +2744,36 @@ public sealed class LiveEntityRuntime
private void ReleaseTeardownRecord(LiveEntityRecord record) private void ReleaseTeardownRecord(LiveEntityRecord record)
{ {
var key = TeardownKey(record); if (_teardownRecords.TryGetValue(
if (_teardownRecords.TryGetValue(key, out LiveEntityRecord? retained) record.Canonical,
out LiveEntityRecord? retained)
&& ReferenceEquals(retained, record)) && ReferenceEquals(retained, record))
{ {
_teardownRecords.Remove(key); _teardownRecords.Remove(record.Canonical);
} }
_directory.ReleaseTeardown(record.Canonical);
CompleteSessionClearIfConverged(); CompleteSessionClearIfConverged();
} }
private bool HasPendingTeardown(uint serverGuid) private bool HasPendingTeardown(uint serverGuid) =>
{ _directory.HasPendingTeardown(serverGuid);
foreach ((uint Guid, ushort Generation) key in _teardownRecords.Keys)
{
if (key.Guid == serverGuid)
return true;
}
return false;
}
private void CompleteSessionClearIfConverged() private void CompleteSessionClearIfConverged()
{ {
if (!_sessionClearPendingFinalization if (!_sessionClearPendingFinalization
|| _recordsByGuid.Count != 0 || !_directory.CompleteSessionClearIfConverged())
|| _teardownRecords.Count != 0)
{ {
return; return;
} }
_materializedWorldEntitiesByGuid.Clear(); _materializedWorldEntitiesByGuid.Clear();
_visibleWorldEntitiesByGuid.Clear(); _visibleWorldEntitiesByGuid.Clear();
_guidByLocalId.Clear();
_animationsByLocalId.Clear(); _animationsByLocalId.Clear();
_remoteMotionByGuid.Clear(); _remoteMotionByGuid.Clear();
_spatialAnimationsByLocalId.Clear(); _spatialAnimationsByLocalId.Clear();
_spatialRemoteMotionByGuid.Clear(); _spatialRemoteMotionByGuid.Clear();
_spatialProjectilesByGuid.Clear(); _spatialProjectilesByGuid.Clear();
_spatialRootObjectsByGuid.Clear(); _spatialRootObjectsByGuid.Clear();
ParentAttachments.Clear();
_inbound.Clear();
_spatial.ClearLiveEntityLifetimeState(); _spatial.ClearLiveEntityLifetimeState();
_sessionClearPendingFinalization = false; _sessionClearPendingFinalization = false;
} }
@ -2815,7 +2837,7 @@ public sealed class LiveEntityRuntime
if (record.WorldEntity is { } finalizedEntity) if (record.WorldEntity is { } finalizedEntity)
{ {
_guidByLocalId.Remove(finalizedEntity.Id); _directory.ReleaseLocalId(record.Canonical);
if (_materializedWorldEntitiesByGuid.TryGetValue( if (_materializedWorldEntitiesByGuid.TryGetValue(
record.ServerGuid, record.ServerGuid,
out WorldEntity? materialized) out WorldEntity? materialized)
@ -2864,4 +2886,80 @@ public sealed class LiveEntityRuntime
record.TeardownInProgress = false; record.TeardownInProgress = false;
} }
} }
/// <summary>
/// Dictionary-shaped compatibility view used only while J3.2 migrates the
/// large App projection surface. RuntimeEntityDirectory remains the sole
/// GUID/incarnation authority; this type stores sidecars by canonical
/// record reference and cannot resolve a GUID independently.
/// </summary>
private sealed class ActiveRecordView
{
private readonly RuntimeEntityDirectory _directory;
private readonly Dictionary<RuntimeEntityRecord, LiveEntityRecord> _sidecars;
public ActiveRecordView(
RuntimeEntityDirectory directory,
Dictionary<RuntimeEntityRecord, LiveEntityRecord> sidecars)
{
_directory = directory;
_sidecars = sidecars;
}
public int Count => _directory.Count;
public IReadOnlyCollection<LiveEntityRecord> Values => _sidecars.Values;
public bool TryGetValue(uint guid, out LiveEntityRecord record)
{
if (_directory.TryGetActive(guid, out RuntimeEntityRecord canonical)
&& _sidecars.TryGetValue(canonical, out LiveEntityRecord? sidecar))
{
record = sidecar;
return true;
}
record = null!;
return false;
}
public bool ContainsKey(uint guid) =>
_directory.TryGetActive(guid, out RuntimeEntityRecord canonical)
&& _sidecars.ContainsKey(canonical);
public LiveEntityRecord? GetValueOrDefault(uint guid) =>
TryGetValue(guid, out LiveEntityRecord record) ? record : null;
public void Add(uint guid, LiveEntityRecord record)
{
if (record.ServerGuid != guid
|| !_directory.TryGetActive(guid, out RuntimeEntityRecord canonical)
|| !ReferenceEquals(canonical, record.Canonical))
{
throw new InvalidOperationException(
$"App projection sidecar 0x{record.ServerGuid:X8} does not match the canonical Runtime identity 0x{guid:X8}.");
}
_sidecars.Add(canonical, record);
}
public bool Remove(uint guid) => Remove(guid, out _);
public bool Remove(uint guid, out LiveEntityRecord? record)
{
if (!_directory.RemoveActive(guid, out RuntimeEntityRecord? canonical)
|| canonical is null)
{
record = null;
return false;
}
if (!_sidecars.Remove(canonical, out record))
{
throw new InvalidOperationException(
$"Canonical Runtime identity 0x{guid:X8} had no App projection sidecar.");
}
return true;
}
}
} }

View file

@ -0,0 +1,428 @@
using AcDream.Core.Net;
using AcDream.Core.Physics;
namespace AcDream.Runtime.Entities;
/// <summary>
/// The single update-thread authority for live server GUIDs, incarnations,
/// accepted wire snapshots, teardown tombstones, and runtime-local identity.
/// It owns no graphical state and can run without loading App or a backend.
/// </summary>
public sealed class RuntimeEntityDirectory
{
public const uint FirstLocalEntityId = 1_000_000u;
public const uint LastLocalEntityId = 0x3FFF_FFFFu;
private readonly InboundPhysicsStateController _inbound = new();
private readonly Dictionary<uint, RuntimeEntityRecord> _activeByGuid = new();
private readonly Dictionary<(uint Guid, ushort Incarnation), RuntimeEntityRecord>
_teardownByIncarnation = new();
private readonly Dictionary<uint, RuntimeEntityRecord> _byLocalId = new();
private readonly Dictionary<uint, ulong> _lifetimeMutationByGuid = new();
private uint _nextLocalEntityId;
public RuntimeEntityDirectory(uint firstLocalEntityId = FirstLocalEntityId)
{
if (firstLocalEntityId is < FirstLocalEntityId or > LastLocalEntityId)
{
throw new ArgumentOutOfRangeException(
nameof(firstLocalEntityId),
$"Live entity ids must stay in 0x{FirstLocalEntityId:X8}..0x{LastLocalEntityId:X8}.");
}
_nextLocalEntityId = firstLocalEntityId;
}
public int Count => _activeByGuid.Count;
public int PendingTeardownCount => _teardownByIncarnation.Count;
public int ClaimedLocalIdCount => _byLocalId.Count;
public ulong SessionLifetimeVersion { get; private set; }
public IReadOnlyCollection<RuntimeEntityRecord> ActiveRecords => _activeByGuid.Values;
public IReadOnlyCollection<RuntimeEntityRecord> TeardownRecords =>
_teardownByIncarnation.Values;
public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _inbound.Snapshots;
public ParentAttachmentState ParentAttachments { get; } = new();
public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming) =>
_inbound.AcceptCreate(incoming);
public bool TryDelete(
AcDream.Core.Net.Messages.DeleteObject.Parsed delete,
bool isLocalPlayer) =>
_inbound.TryDelete(delete, isLocalPlayer);
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
_inbound.TryGetSnapshot(guid, out spawn);
public bool TryGetActive(uint guid, out RuntimeEntityRecord record) =>
_activeByGuid.TryGetValue(guid, out record!);
public bool IsCurrent(RuntimeEntityRecord record) =>
_activeByGuid.TryGetValue(record.ServerGuid, out RuntimeEntityRecord? current)
&& ReferenceEquals(current, record);
public bool TryGetByLocalId(uint localEntityId, out RuntimeEntityRecord record) =>
_byLocalId.TryGetValue(localEntityId, out record!);
public bool TryGetTeardown(
uint guid,
ushort incarnation,
out RuntimeEntityRecord record) =>
_teardownByIncarnation.TryGetValue((guid, incarnation), out record!);
public RuntimeEntityRecord AddActive(WorldSession.EntitySpawn snapshot)
{
if (_activeByGuid.ContainsKey(snapshot.Guid))
{
throw new InvalidOperationException(
$"Live entity 0x{snapshot.Guid:X8} already has an active incarnation.");
}
var record = new RuntimeEntityRecord(snapshot);
_activeByGuid.Add(snapshot.Guid, record);
return record;
}
public bool RemoveActive(uint guid, out RuntimeEntityRecord? record) =>
_activeByGuid.Remove(guid, out record);
public bool RemoveActive(RuntimeEntityRecord expected)
{
if (!IsCurrent(expected))
return false;
return _activeByGuid.Remove(expected.ServerGuid);
}
public void RetainTeardown(RuntimeEntityRecord record)
{
var key = (record.ServerGuid, record.Incarnation);
if (_teardownByIncarnation.TryGetValue(
key,
out RuntimeEntityRecord? retained)
&& !ReferenceEquals(retained, record))
{
throw new InvalidOperationException(
$"Live entity teardown tombstone collision for 0x{record.ServerGuid:X8} generation {record.Incarnation}.");
}
_teardownByIncarnation[key] = record;
}
public void ReleaseTeardown(RuntimeEntityRecord record)
{
var key = (record.ServerGuid, record.Incarnation);
if (_teardownByIncarnation.TryGetValue(
key,
out RuntimeEntityRecord? retained)
&& ReferenceEquals(retained, record))
{
_teardownByIncarnation.Remove(key);
}
}
public bool HasPendingTeardown(uint serverGuid)
{
foreach ((uint Guid, ushort Incarnation) key in _teardownByIncarnation.Keys)
{
if (key.Guid == serverGuid)
return true;
}
return false;
}
public uint ClaimLocalId(RuntimeEntityRecord record)
{
if (!IsKnown(record))
{
throw new InvalidOperationException(
"A local id can only be claimed for an active or retained incarnation.");
}
if (record.LocalEntityId is { } existing)
return existing;
uint start = _nextLocalEntityId;
do
{
uint candidate = _nextLocalEntityId;
_nextLocalEntityId = candidate == LastLocalEntityId
? FirstLocalEntityId
: candidate + 1u;
if (_byLocalId.ContainsKey(candidate))
continue;
_byLocalId.Add(candidate, record);
record.LocalEntityId = candidate;
return candidate;
}
while (_nextLocalEntityId != start);
throw new InvalidOperationException("The live entity id namespace is exhausted.");
}
public bool ReleaseLocalId(RuntimeEntityRecord record)
{
if (record.LocalEntityId is not { } localId)
return false;
if (_byLocalId.TryGetValue(localId, out RuntimeEntityRecord? retained)
&& ReferenceEquals(retained, record))
{
_byLocalId.Remove(localId);
}
record.LocalEntityId = null;
return true;
}
public ulong AdvanceLifetimeMutation(uint serverGuid)
{
ulong next = _lifetimeMutationByGuid.GetValueOrDefault(serverGuid) + 1UL;
_lifetimeMutationByGuid[serverGuid] = next;
return next;
}
public ulong CurrentLifetimeMutation(uint serverGuid) =>
_lifetimeMutationByGuid.GetValueOrDefault(serverGuid);
public void BeginSessionClear()
{
SessionLifetimeVersion++;
_lifetimeMutationByGuid.Clear();
ParentAttachments.Clear();
_inbound.Clear();
}
public bool CompleteSessionClearIfConverged()
{
if (_activeByGuid.Count != 0 || _teardownByIncarnation.Count != 0)
return false;
_byLocalId.Clear();
ParentAttachments.Clear();
_inbound.Clear();
return true;
}
public void RefreshSnapshot(
RuntimeEntityRecord record,
WorldSession.EntitySpawn accepted,
bool refreshPosition = false)
{
EnsureKnown(record);
record.Snapshot = accepted;
record.RefreshDerivedState(refreshPosition);
}
public void AdvanceCreateAuthority(RuntimeEntityRecord record)
{
EnsureKnown(record);
record.AdvanceCreateAuthority();
}
public void AdvancePositionAuthority(RuntimeEntityRecord record)
{
EnsureKnown(record);
record.AdvancePositionAuthority();
}
public void AdvanceVectorAuthority(RuntimeEntityRecord record)
{
EnsureKnown(record);
record.AdvanceVectorAuthority();
}
public void AdvanceMovementAuthority(RuntimeEntityRecord record)
{
EnsureKnown(record);
record.AdvanceMovementAuthority();
}
public void AdvanceObjDescAuthority(RuntimeEntityRecord record)
{
EnsureKnown(record);
record.AdvanceObjDescAuthority();
}
public RetailPhysicsStateTransition ApplyRawPhysicsState(
RuntimeEntityRecord record,
uint rawState)
{
EnsureKnown(record);
return record.ApplyRawPhysicsState(rawState);
}
public bool TryDequeueStateTransition(
RuntimeEntityRecord record,
out RetailPhysicsStateTransition transition)
{
EnsureKnown(record);
return record.TryDequeueStateTransition(out transition);
}
public void SetChildNoDraw(RuntimeEntityRecord record, bool noDraw)
{
EnsureKnown(record);
record.SetChildNoDraw(noDraw);
}
public void SuspendObjectClock(RuntimeEntityRecord record)
{
EnsureKnown(record);
record.SuspendObjectClock();
}
public void ResetObjectClockForEnterWorld(RuntimeEntityRecord record, bool isStatic)
{
EnsureKnown(record);
record.ResetObjectClockForEnterWorld(isStatic);
}
public void SetFullCell(
RuntimeEntityRecord record,
uint fullCellId,
uint canonicalLandblockId)
{
EnsureKnown(record);
record.FullCellId = fullCellId;
record.CanonicalLandblockId = canonicalLandblockId;
}
public void SetFinalPhysicsState(
RuntimeEntityRecord record,
PhysicsStateFlags state)
{
EnsureKnown(record);
record.FinalPhysicsState = state;
}
public void SetHasPartArray(RuntimeEntityRecord record, bool value)
{
EnsureKnown(record);
record.HasPartArray = value;
}
public void SetPhysicsBody(
RuntimeEntityRecord record,
AcDream.Core.Physics.PhysicsBody? body)
{
EnsureKnown(record);
record.PhysicsBody = body;
}
public void SetPhysicsBodyAcquisitionInProgress(
RuntimeEntityRecord record,
bool value)
{
EnsureKnown(record);
record.PhysicsBodyAcquisitionInProgress = value;
}
public void SetPhysicsHost(
RuntimeEntityRecord record,
AcDream.Core.Physics.Motion.IPhysicsObjHost? host)
{
EnsureKnown(record);
record.PhysicsHost = host;
}
public void SetDeleteAcceptedForTeardown(
RuntimeEntityRecord record,
bool value)
{
EnsureKnown(record);
record.DeleteAcceptedForTeardown = value;
}
public bool TryApplyObjDesc(
AcDream.Core.Net.Messages.ObjDescEvent.Parsed update,
out WorldSession.EntitySpawn accepted) =>
_inbound.TryApplyObjDesc(update, out accepted);
public bool TryApplyPickup(
AcDream.Core.Net.Messages.PickupEvent.Parsed update,
out WorldSession.EntitySpawn accepted) =>
_inbound.TryApplyPickup(update, out accepted);
public bool TryApplyCreateParent(
CreateParentUpdate update,
out WorldSession.EntitySpawn accepted) =>
_inbound.TryApplyCreateParent(update, out accepted);
public bool TryApplyParent(
AcDream.Core.Net.Messages.ParentEvent.Parsed update,
out WorldSession.EntitySpawn accepted) =>
_inbound.TryApplyParent(update, out accepted);
public bool TryCommitParent(
uint childGuid,
uint parentGuid,
uint parentLocation,
uint placementId,
ushort positionSequence,
out WorldSession.EntitySpawn accepted) =>
_inbound.TryCommitParent(
childGuid,
parentGuid,
parentLocation,
placementId,
positionSequence,
out accepted);
public bool TryApplyMotion(
WorldSession.EntityMotionUpdate update,
bool retainPayload,
out WorldSession.EntitySpawn accepted,
out AcceptedPhysicsTimestamps timestamps) =>
_inbound.TryApplyMotion(update, retainPayload, out accepted, out timestamps);
public bool TryApplyVector(
AcDream.Core.Net.Messages.VectorUpdate.Parsed update,
out WorldSession.EntitySpawn accepted) =>
_inbound.TryApplyVector(update, out accepted);
public bool TryApplyState(
AcDream.Core.Net.Messages.SetState.Parsed update,
out WorldSession.EntitySpawn accepted) =>
_inbound.TryApplyState(update, out accepted);
public bool TryApplyPosition(
WorldSession.EntityPositionUpdate update,
bool isLocalPlayer,
System.Numerics.Quaternion? forcePositionRotation,
System.Numerics.Vector3? currentLocalVelocity,
out PositionTimestampDisposition disposition,
out WorldSession.EntitySpawn accepted,
out AcceptedPhysicsTimestamps timestamps) =>
_inbound.TryApplyPosition(
update,
isLocalPlayer,
forcePositionRotation,
currentLocalVelocity,
out disposition,
out accepted,
out timestamps);
public bool IsFreshTeleportStart(uint guid, ushort teleportSequence) =>
_inbound.IsFreshTeleportStart(guid, teleportSequence);
private bool IsKnown(RuntimeEntityRecord record)
{
if (IsCurrent(record))
return true;
return _teardownByIncarnation.TryGetValue(
(record.ServerGuid, record.Incarnation),
out RuntimeEntityRecord? retained)
&& ReferenceEquals(retained, record);
}
private void EnsureKnown(RuntimeEntityRecord record)
{
ArgumentNullException.ThrowIfNull(record);
if (!IsKnown(record))
{
throw new InvalidOperationException(
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} is no longer owned by the directory.");
}
}
}

View file

@ -0,0 +1,160 @@
using AcDream.Core.Net;
using AcDream.Core.Physics;
namespace AcDream.Runtime.Entities;
/// <summary>
/// Stable identity issued by the canonical runtime directory. The local id is
/// claimed when a graphical host first materializes the incarnation; a
/// no-window host may claim it immediately.
/// </summary>
public readonly record struct RuntimeEntityKey(
uint LocalEntityId,
ushort Incarnation);
/// <summary>
/// Presentation-free canonical state for one accepted server-object
/// incarnation. App projection, animation, effects, visibility, and hydration
/// state must never be stored here.
/// </summary>
public sealed class RuntimeEntityRecord
{
private readonly Queue<RetailPhysicsStateTransition> _pendingStateTransitions = new();
internal RuntimeEntityRecord(WorldSession.EntitySpawn snapshot)
{
ServerGuid = snapshot.Guid;
Snapshot = snapshot;
RefreshDerivedState();
PositionAuthorityVersion = snapshot.Position is null ? 0UL : 1UL;
VectorAuthorityVersion = snapshot.Physics is null ? 0UL : 1UL;
VelocityAuthorityVersion = snapshot.Position is null
&& snapshot.Physics is null
? 0UL
: 1UL;
MovementAuthorityVersion = 1UL;
ObjDescAuthorityVersion = 1UL;
FinalPhysicsState = RetailPhysicsStateTransitions.ConstructorState;
ApplyRawPhysicsState(RawPhysicsState);
}
public uint ServerGuid { get; }
public ushort Incarnation => Snapshot.InstanceSequence;
public ushort Generation => Incarnation;
public WorldSession.EntitySpawn Snapshot { get; internal set; }
public uint? LocalEntityId { get; internal set; }
public RuntimeEntityKey? Key => LocalEntityId is { } localId
? new RuntimeEntityKey(localId, Incarnation)
: null;
public uint FullCellId { get; internal set; }
public uint CanonicalLandblockId { get; internal set; }
public uint RawPhysicsState { get; internal set; }
public PhysicsStateFlags FinalPhysicsState { get; internal set; }
/// <summary>
/// Incarnation-stable retail <c>CPhysicsObj::update_time</c> owner.
/// </summary>
public RetailObjectQuantumClock ObjectClock { get; } = new();
public ulong ObjectClockEpoch { get; private set; }
/// <summary>
/// True after this incarnation has successfully constructed its retail
/// <c>CPartArray</c>. This is a simulation/lifetime fact, not a renderer
/// object reference.
/// </summary>
public bool HasPartArray { get; internal set; }
public PhysicsBody? PhysicsBody { get; internal set; }
public bool PhysicsBodyAcquisitionInProgress { get; internal set; }
public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost { get; internal set; }
public ulong PositionAuthorityVersion { get; private set; }
public ulong StateAuthorityVersion { get; private set; }
public ulong VectorAuthorityVersion { get; private set; }
public ulong VelocityAuthorityVersion { get; private set; }
public ulong MovementAuthorityVersion { get; private set; }
public ulong ObjDescAuthorityVersion { get; private set; }
public ulong CreateIntegrationVersion { get; private set; } = 1UL;
public bool DeleteAcceptedForTeardown { get; internal set; }
internal bool TryDequeueStateTransition(out RetailPhysicsStateTransition transition) =>
_pendingStateTransitions.TryDequeue(out transition);
internal void SuspendObjectClock()
{
ObjectClock.Deactivate();
ObjectClockEpoch++;
}
internal void ResetObjectClockForEnterWorld(bool isStatic)
{
ObjectClock.ResetForEnterWorld(isStatic);
ObjectClockEpoch++;
}
internal RetailPhysicsStateTransition ApplyRawPhysicsState(uint rawState)
{
StateAuthorityVersion++;
RawPhysicsState = rawState;
RetailPhysicsStateTransition transition = RetailPhysicsStateTransitions.Apply(
FinalPhysicsState,
(PhysicsStateFlags)rawState);
FinalPhysicsState = transition.FinalState;
if (PhysicsBody is not null)
PhysicsBody.State = FinalPhysicsState;
_pendingStateTransitions.Enqueue(transition);
return transition;
}
internal void AdvancePositionAuthority()
{
PositionAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvanceVectorAuthority()
{
VectorAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvanceMovementAuthority()
{
MovementAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvanceObjDescAuthority() => ObjDescAuthorityVersion++;
internal void AdvanceCreateAuthority()
{
PositionAuthorityVersion++;
StateAuthorityVersion++;
VectorAuthorityVersion++;
VelocityAuthorityVersion++;
MovementAuthorityVersion++;
ObjDescAuthorityVersion++;
CreateIntegrationVersion++;
}
internal void SetChildNoDraw(bool noDraw)
{
FinalPhysicsState = noDraw
? FinalPhysicsState | PhysicsStateFlags.NoDraw
: FinalPhysicsState & ~PhysicsStateFlags.NoDraw;
if (PhysicsBody is not null)
PhysicsBody.State = FinalPhysicsState;
}
internal void RefreshDerivedState(bool refreshPosition = true)
{
if (refreshPosition && Snapshot.Position is { } position)
{
FullCellId = position.LandblockId;
CanonicalLandblockId = (FullCellId & 0xFFFF0000u) | 0xFFFFu;
}
RawPhysicsState = Snapshot.Physics?.RawState
?? Snapshot.PhysicsState
?? 0u;
}
}

View file

@ -0,0 +1,76 @@
using System.Reflection;
using AcDream.App.World;
using AcDream.Runtime.Entities;
namespace AcDream.App.Tests.World;
public sealed class RuntimeEntityOwnershipTests
{
[Fact]
public void LiveEntityRuntime_ComposesCanonicalRuntimeDirectory()
{
FieldInfo directory = typeof(LiveEntityRuntime).GetField(
"_directory",
BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException("Missing canonical entity directory.");
FieldInfo sidecars = typeof(LiveEntityRuntime).GetField(
"_activeRecords",
BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException("Missing App projection sidecar store.");
Assert.Equal(typeof(RuntimeEntityDirectory), directory.FieldType);
Assert.Equal(typeof(RuntimeEntityRecord), sidecars.FieldType.GetGenericArguments()[0]);
Assert.Equal(typeof(LiveEntityRecord), sidecars.FieldType.GetGenericArguments()[1]);
}
[Fact]
public void AppCompatibilityLookup_IsNotASecondGuidDictionary()
{
FieldInfo compatibilityView = typeof(LiveEntityRuntime).GetField(
"_recordsByGuid",
BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException("Missing migration compatibility view.");
Assert.False(compatibilityView.FieldType.IsGenericType);
Assert.DoesNotContain(
compatibilityView.FieldType.GetInterfaces(),
type => type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(IDictionary<,>));
}
[Fact]
public void CanonicalRuntimeRecord_HasNoProjectionOrBackendSurface()
{
string[] forbiddenPropertyNames =
[
"WorldEntity",
"AnimationRuntime",
"RemoteMotionRuntime",
"ProjectileRuntime",
"EffectProfile",
"ResourcesRegistered",
"IsSpatiallyProjected",
"IsSpatiallyVisible",
];
PropertyInfo[] properties = typeof(RuntimeEntityRecord).GetProperties(
BindingFlags.Instance | BindingFlags.Public);
Assert.DoesNotContain(
properties,
property => forbiddenPropertyNames.Contains(
property.Name,
StringComparer.Ordinal));
Assert.DoesNotContain(
properties,
property => IsPresentationType(property.PropertyType));
}
private static bool IsPresentationType(Type type)
{
string? ns = type.Namespace;
return ns?.StartsWith("AcDream.App", StringComparison.Ordinal) == true
|| ns?.StartsWith("AcDream.UI", StringComparison.Ordinal) == true
|| ns?.StartsWith("Silk.NET", StringComparison.Ordinal) == true
|| ns?.StartsWith("ImGuiNET", StringComparison.Ordinal) == true;
}
}

View file

@ -0,0 +1,231 @@
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
namespace AcDream.Runtime.Tests.Entities;
public sealed class RuntimeEntityDirectoryTests
{
[Fact]
public void AddActive_OwnsOneCurrentIncarnationPerGuid()
{
var directory = new RuntimeEntityDirectory();
RuntimeEntityRecord first = directory.AddActive(Spawn(0x70000001u, 1));
Assert.True(directory.TryGetActive(first.ServerGuid, out RuntimeEntityRecord found));
Assert.Same(first, found);
Assert.True(directory.IsCurrent(first));
Assert.Throws<InvalidOperationException>(
() => directory.AddActive(Spawn(first.ServerGuid, 2)));
}
[Fact]
public void RemoveAndRetainTombstone_AllowsReplacementWithoutLosingExactOldIncarnation()
{
var directory = new RuntimeEntityDirectory();
RuntimeEntityRecord old = directory.AddActive(Spawn(0x70000002u, 7));
Assert.True(directory.RemoveActive(old.ServerGuid, out RuntimeEntityRecord? removed));
Assert.Same(old, removed);
directory.RetainTeardown(old);
RuntimeEntityRecord replacement = directory.AddActive(Spawn(old.ServerGuid, 8));
Assert.True(directory.TryGetActive(old.ServerGuid, out RuntimeEntityRecord current));
Assert.Same(replacement, current);
Assert.True(directory.TryGetTeardown(old.ServerGuid, 7, out RuntimeEntityRecord tombstone));
Assert.Same(old, tombstone);
Assert.False(directory.IsCurrent(old));
Assert.True(directory.IsCurrent(replacement));
}
[Fact]
public void LocalIdentity_IsOwnedByDirectoryAndReverseLookupRejectsReleasedIncarnation()
{
var directory = new RuntimeEntityDirectory();
RuntimeEntityRecord old = directory.AddActive(Spawn(0x70000003u, 1));
uint oldLocalId = directory.ClaimLocalId(old);
Assert.True(directory.TryGetByLocalId(oldLocalId, out RuntimeEntityRecord foundOld));
Assert.Same(old, foundOld);
Assert.True(directory.RemoveActive(old.ServerGuid, out _));
directory.RetainTeardown(old);
RuntimeEntityRecord replacement = directory.AddActive(Spawn(old.ServerGuid, 2));
uint replacementLocalId = directory.ClaimLocalId(replacement);
Assert.NotEqual(oldLocalId, replacementLocalId);
Assert.True(directory.ReleaseLocalId(old));
Assert.False(directory.TryGetByLocalId(oldLocalId, out _));
Assert.True(directory.TryGetByLocalId(
replacementLocalId,
out RuntimeEntityRecord foundReplacement));
Assert.Same(replacement, foundReplacement);
}
[Fact]
public void LocalIdentity_WrapsWithinReservedNamespaceWithoutCollision()
{
var directory = new RuntimeEntityDirectory(RuntimeEntityDirectory.LastLocalEntityId);
RuntimeEntityRecord first = directory.AddActive(Spawn(0x70000004u, 1));
RuntimeEntityRecord second = directory.AddActive(Spawn(0x70000005u, 1));
Assert.Equal(
RuntimeEntityDirectory.LastLocalEntityId,
directory.ClaimLocalId(first));
Assert.Equal(
RuntimeEntityDirectory.FirstLocalEntityId,
directory.ClaimLocalId(second));
}
[Fact]
public void SessionClear_AdvancesEpochAndPreservesRetryableTombstone()
{
var directory = new RuntimeEntityDirectory();
RuntimeEntityRecord record = directory.AddActive(Spawn(0x70000006u, 3));
ulong operation = directory.AdvanceLifetimeMutation(record.ServerGuid);
Assert.Equal(operation, directory.CurrentLifetimeMutation(record.ServerGuid));
Assert.True(directory.RemoveActive(record.ServerGuid, out _));
directory.RetainTeardown(record);
directory.BeginSessionClear();
Assert.Equal(1UL, directory.SessionLifetimeVersion);
Assert.Equal(0UL, directory.CurrentLifetimeMutation(record.ServerGuid));
Assert.False(directory.CompleteSessionClearIfConverged());
Assert.True(directory.TryGetTeardown(record.ServerGuid, 3, out _));
directory.ReleaseTeardown(record);
Assert.True(directory.CompleteSessionClearIfConverged());
}
[Fact]
public void AcceptedCreateSnapshotAndAuthorityLiveOnCanonicalRecord()
{
var directory = new RuntimeEntityDirectory();
WorldSession.EntitySpawn spawn = Spawn(0x70000007u, 4);
InboundCreateResult accepted = directory.AcceptCreate(spawn);
RuntimeEntityRecord record = directory.AddActive(accepted.Snapshot);
ulong priorCreateVersion = record.CreateIntegrationVersion;
directory.AdvanceCreateAuthority(record);
directory.RefreshSnapshot(
record,
accepted.Snapshot with { Name = "refreshed" },
refreshPosition: true);
Assert.Equal("refreshed", record.Snapshot.Name);
Assert.Equal(priorCreateVersion + 1UL, record.CreateIntegrationVersion);
Assert.Equal(spawn.Position!.Value.LandblockId, record.FullCellId);
}
[Fact]
public void DeleteGateAndIdentityRemovalRemainSeparateRetryableEdges()
{
var directory = new RuntimeEntityDirectory();
WorldSession.EntitySpawn spawn = Spawn(0x70000008u, 9);
directory.AcceptCreate(spawn);
RuntimeEntityRecord record = directory.AddActive(spawn);
Assert.True(directory.TryDelete(
new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence),
isLocalPlayer: false));
Assert.True(directory.IsCurrent(record));
Assert.True(directory.RemoveActive(record));
directory.RetainTeardown(record);
Assert.True(directory.TryGetTeardown(
spawn.Guid,
spawn.InstanceSequence,
out RuntimeEntityRecord retained));
Assert.Same(record, retained);
}
[Fact]
public void CanonicalRecord_PublicSurfaceContainsNoAppOrBackendTypes()
{
Type canonical = typeof(RuntimeEntityRecord);
Type[] exposed = canonical
.GetProperties()
.Select(property => property.PropertyType)
.Concat(canonical.GetMethods().Select(method => method.ReturnType))
.ToArray();
Assert.DoesNotContain(exposed, type =>
type.Namespace?.StartsWith("AcDream.App", StringComparison.Ordinal) == true
|| type.Namespace?.StartsWith("Silk.NET", StringComparison.Ordinal) == true
|| type.Namespace?.StartsWith("ImGuiNET", StringComparison.Ordinal) == true);
}
[Fact]
public void Constructor_RejectsIdsOutsideLiveNamespace()
{
Assert.Throws<ArgumentOutOfRangeException>(
() => new RuntimeEntityDirectory(
RuntimeEntityDirectory.FirstLocalEntityId - 1u));
Assert.Throws<ArgumentOutOfRangeException>(
() => new RuntimeEntityDirectory(
RuntimeEntityDirectory.LastLocalEntityId + 1u));
}
private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance)
{
var position = new CreateObject.ServerPosition(
0x0101FFFFu,
10f,
20f,
5f,
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: instance);
var physics = new PhysicsSpawnData(
RawState: 0x408u,
Position: position,
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,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"fixture",
null,
null,
0x09000001u,
PhysicsState: 0x408u,
InstanceSequence: instance,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
}