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>
/// The one logical record for a server object incarnation. The record survives
/// loaded/pending landblock movement and temporary loss of its render bucket.
/// Only an accepted DeleteObject, session reset, newer INSTANCE_TS, or the
/// active side of the leave-visibility lifecycle ends it. ACE-compatible
/// cold snapshot ownership lives outside this active runtime.
/// App projection sidecar for one canonical <see cref="RuntimeEntityRecord"/>.
/// The canonical record survives loaded/pending landblock movement and
/// temporary loss of its render bucket; this sidecar retains only graphical
/// components, visibility, hydration, and teardown progress.
/// </summary>
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;
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);
_directory = directory ?? throw new ArgumentNullException(nameof(directory));
Canonical = canonical ?? throw new ArgumentNullException(nameof(canonical));
}
public uint ServerGuid { get; }
public ushort Generation => Snapshot.InstanceSequence;
public WorldSession.EntitySpawn Snapshot { get; internal set; }
/// <summary>
/// Detached presentation fixture used by App component tests which do not
/// 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 uint? LocalEntityId => WorldEntity?.Id;
public uint FullCellId { get; internal set; }
public uint CanonicalLandblockId { get; internal set; }
public uint RawPhysicsState { get; internal set; }
public PhysicsStateFlags FinalPhysicsState { get; internal set; }
public uint? LocalEntityId => Canonical.LocalEntityId ?? WorldEntity?.Id;
public uint FullCellId
{
get => Canonical.FullCellId;
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>
/// Incarnation-stable retail <c>CPhysicsObj::update_time</c> owner. It
/// survives animation and MovementManager attachment/replacement, spatial
/// rebucketing, and appearance rehydration, and dies with this record.
/// </summary>
public RetailObjectQuantumClock ObjectClock { get; } = new();
public RetailObjectQuantumClock ObjectClock => Canonical.ObjectClock;
/// <summary>
/// Changes whenever this record leaves or re-enters the ordinary-object
/// 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
/// ordinary same-frame cell move.
/// </summary>
internal ulong ObjectClockEpoch { get; private set; }
internal ulong ObjectClockEpoch => Canonical.ObjectClockEpoch;
/// <summary>
/// True after this incarnation has successfully constructed its retail
/// <c>CPartArray</c>. This is an object-lifetime fact, not an animation-
/// runtime proxy: static poses and objects without a MotionTable still
/// own a PartArray and participate in the 96-unit Active gate.
/// </summary>
public bool HasPartArray { get; internal set; }
public PhysicsBody? PhysicsBody { get; internal set; }
internal bool PhysicsBodyAcquisitionInProgress { get; set; }
public bool HasPartArray
{
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 ILiveEntityRemoteMotionRuntime? RemoteMotionRuntime { get; internal 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; }
public ILiveEntityProjectileRuntime? ProjectileRuntime { 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
/// while an older packet is crossing presentation or spatial callbacks.
/// </summary>
internal ulong PositionAuthorityVersion { get; private set; }
internal ulong StateAuthorityVersion { get; private set; }
internal ulong VectorAuthorityVersion { get; private set; }
internal ulong VelocityAuthorityVersion { get; private set; }
internal ulong MovementAuthorityVersion { get; private set; }
internal ulong ObjDescAuthorityVersion { get; private set; }
internal ulong PositionAuthorityVersion => Canonical.PositionAuthorityVersion;
internal ulong StateAuthorityVersion => Canonical.StateAuthorityVersion;
internal ulong VectorAuthorityVersion => Canonical.VectorAuthorityVersion;
internal ulong VelocityAuthorityVersion => Canonical.VelocityAuthorityVersion;
internal ulong MovementAuthorityVersion => Canonical.MovementAuthorityVersion;
internal ulong ObjDescAuthorityVersion => Canonical.ObjDescAuthorityVersion;
/// <summary>
/// Advances for every accepted CreateObject affecting this incarnation,
/// including a fresher same-INSTANCE_TS retransmit. App integration uses
/// it to distinguish two transactions that intentionally share identity.
/// </summary>
internal ulong CreateIntegrationVersion { get; private set; } = 1UL;
internal ulong CreateIntegrationVersion => Canonical.CreateIntegrationVersion;
public bool WorldSpawnPublished { get; internal set; }
/// <summary>
/// True only after the incarnation's projection, collision/animation
@ -330,82 +376,45 @@ public sealed class LiveEntityRecord
internal LiveEntityTeardownPlan? RuntimeComponentTeardownPlan { get; set; }
internal bool SpatialProjectionTeardownCompleted { 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) =>
_pendingStateTransitions.TryDequeue(out transition);
_directory.TryDequeueStateTransition(Canonical, out transition);
internal void SuspendObjectClock()
{
ObjectClock.Deactivate();
ObjectClockEpoch++;
}
internal void SuspendObjectClock() => _directory.SuspendObjectClock(Canonical);
internal void ResetObjectClockForEnterWorld(bool isStatic)
{
ObjectClock.ResetForEnterWorld(isStatic);
ObjectClockEpoch++;
}
internal void ResetObjectClockForEnterWorld(bool isStatic) =>
_directory.ResetObjectClockForEnterWorld(Canonical, isStatic);
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 RetailPhysicsStateTransition ApplyRawPhysicsState(uint rawState) =>
_directory.ApplyRawPhysicsState(Canonical, rawState);
internal void AdvancePositionAuthority()
{
PositionAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvancePositionAuthority() =>
_directory.AdvancePositionAuthority(Canonical);
internal void AdvanceVectorAuthority()
{
VectorAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvanceVectorAuthority() =>
_directory.AdvanceVectorAuthority(Canonical);
internal void AdvanceMovementAuthority()
{
MovementAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvanceMovementAuthority() =>
_directory.AdvanceMovementAuthority(Canonical);
internal void AdvanceObjDescAuthority()
{
ObjDescAuthorityVersion++;
_directory.AdvanceObjDescAuthority(Canonical);
AppearanceProjectionSynchronizationPending = true;
if (AppearanceHydrationInProgress)
AppearanceHydrationRetryRequested = true;
}
internal void AdvanceCreateAuthority()
{
PositionAuthorityVersion++;
StateAuthorityVersion++;
VectorAuthorityVersion++;
VelocityAuthorityVersion++;
MovementAuthorityVersion++;
ObjDescAuthorityVersion++;
CreateIntegrationVersion++;
}
internal void AdvanceCreateAuthority() =>
_directory.AdvanceCreateAuthority(Canonical);
internal void SetChildNoDraw(bool noDraw)
{
FinalPhysicsState = noDraw
? FinalPhysicsState | PhysicsStateFlags.NoDraw
: FinalPhysicsState & ~PhysicsStateFlags.NoDraw;
if (PhysicsBody is not null)
PhysicsBody.State = FinalPhysicsState;
}
internal void SetChildNoDraw(bool noDraw) =>
_directory.SetChildNoDraw(Canonical, noDraw);
/// <summary>
/// Cell used when a streamed landblock asks live objects to restore their
@ -416,17 +425,8 @@ public sealed class LiveEntityRecord
? FullCellId
: Snapshot.Position?.LandblockId ?? FullCellId;
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;
}
internal void RefreshDerivedState(bool refreshPosition = true) =>
_directory.RefreshSnapshot(Canonical, Snapshot, refreshPosition);
}
public readonly record struct LiveEntityRegistrationResult(
@ -437,10 +437,13 @@ public readonly record struct LiveEntityRegistrationResult(
Exception? PriorGenerationCleanupFailure = null);
/// <summary>
/// Update-thread owner of live server-object identity, accepted state, runtime
/// components, and logical 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.
/// Update-thread App projection/lifecycle host over the canonical
/// <see cref="RuntimeEntityDirectory"/>. Runtime owns server GUIDs,
/// incarnations, accepted snapshots, timestamps, tombstones, and local IDs;
/// 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>
/// Retail keeps one object per accepted INSTANCE_TS: same-generation
@ -454,23 +457,31 @@ public readonly record struct LiveEntityRegistrationResult(
/// </summary>
public sealed class LiveEntityRuntime
{
public const uint FirstLiveEntityId = 1_000_000u;
public const uint LastLiveEntityId = 0x3FFF_FFFFu;
public const uint FirstLiveEntityId = RuntimeEntityDirectory.FirstLocalEntityId;
public const uint LastLiveEntityId = RuntimeEntityDirectory.LastLocalEntityId;
private readonly GpuWorldState _spatial;
private readonly ILiveEntityResourceLifecycle _resources;
private readonly ILiveEntityRuntimeComponentLifecycle _runtimeComponentLifecycle;
private readonly InboundPhysicsStateController _inbound = new();
private readonly Dictionary<uint, LiveEntityRecord> _recordsByGuid = new();
private readonly RuntimeEntityDirectory _directory;
// 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
// so a newer GUID generation can be accepted re-entrantly. They remain
// canonical teardown tombstones until every resource owner confirms
// 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();
private readonly Dictionary<uint, WorldEntity> _materializedWorldEntitiesByGuid = 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, ILiveEntityRemoteMotionRuntime> _remoteMotionByGuid = new();
// Logical components survive retail's 25-second leave-visibility lifetime.
@ -489,10 +500,7 @@ public sealed class LiveEntityRuntime
private bool _sessionClearPendingFinalization;
private bool _isRegisteringResources;
private int _logicalTeardownDepth;
private ulong _sessionLifetimeVersion;
private readonly Dictionary<uint, ulong> _lifetimeMutationVersionByGuid = new();
private uint _rebucketingGuid;
private uint _nextLocalEntityId;
public LiveEntityRuntime(
GpuWorldState spatial,
@ -529,18 +537,15 @@ public sealed class LiveEntityRuntime
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
_runtimeComponentLifecycle = runtimeComponentLifecycle
?? throw new ArgumentNullException(nameof(runtimeComponentLifecycle));
if (firstLocalEntityId is < FirstLiveEntityId or > LastLiveEntityId)
throw new ArgumentOutOfRangeException(
nameof(firstLocalEntityId),
$"Live entity ids must stay in 0x{FirstLiveEntityId:X8}..0x{LastLiveEntityId:X8}.");
_nextLocalEntityId = firstLocalEntityId;
_directory = new RuntimeEntityDirectory(firstLocalEntityId);
_recordsByGuid = new ActiveRecordView(_directory, _activeRecords);
_spatial.LiveProjectionVisibilityChanged += OnSpatialVisibilityChanged;
}
public int Count => _recordsByGuid.Count;
public int PendingTeardownCount => _teardownRecords.Count;
public int MaterializedCount => _guidByLocalId.Count;
public IReadOnlyCollection<LiveEntityRecord> Records => _recordsByGuid.Values;
public int Count => _directory.Count;
public int PendingTeardownCount => _directory.PendingTeardownCount;
public int MaterializedCount => _directory.ClaimedLocalIdCount;
public IReadOnlyCollection<LiveEntityRecord> Records => _activeRecords.Values;
/// <summary>
/// Every materialized top-level world projection, including an object
/// parked in a pending (currently unloaded) spatial bucket. Network and
@ -558,7 +563,7 @@ public sealed class LiveEntityRuntime
/// </summary>
public IReadOnlyDictionary<uint, WorldEntity> WorldEntities =>
_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, ILiveEntityRemoteMotionRuntime> RemoteMotionRuntimes => _remoteMotionByGuid;
public IReadOnlyDictionary<uint, ILiveEntityAnimationRuntime> SpatialAnimationRuntimes =>
@ -569,7 +574,7 @@ public sealed class LiveEntityRuntime
_spatialProjectilesByGuid;
internal IReadOnlyDictionary<uint, LiveEntityRecord> SpatialRootObjects =>
_spatialRootObjectsByGuid;
public ParentAttachmentState ParentAttachments { get; } = new();
public ParentAttachmentState ParentAttachments => _directory.ParentAttachments;
/// <summary>
/// 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.");
}
InboundCreateResult result = _inbound.AcceptCreate(incoming);
InboundCreateResult result = _directory.AcceptCreate(incoming);
if (result.Disposition is CreateObjectTimestampDisposition.StaleGeneration)
return new LiveEntityRegistrationResult(result, null, false, false);
ulong sessionVersion = _sessionLifetimeVersion;
ulong sessionVersion = _directory.SessionLifetimeVersion;
ulong operationVersion = AdvanceLifetimeMutation(incoming.Guid);
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
// with the same (GUID, INSTANCE_TS) while its partial resources are
// still converging; a later retransmit can recover after teardown.
if (_teardownRecords.ContainsKey(
(incoming.Guid, result.Snapshot.InstanceSequence)))
if (_directory.TryGetTeardown(
incoming.Guid,
result.Snapshot.InstanceSequence,
out _))
{
return new LiveEntityRegistrationResult(
result,
@ -620,7 +627,9 @@ public sealed class LiveEntityRuntime
// Defensive repair for a caller that cleared only the logical map.
// 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);
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
// record/snapshot (delete/reset); reinstalling it would resurrect an
// incarnation after an accepted terminal event.
if (_sessionLifetimeVersion != sessionVersion
if (_directory.SessionLifetimeVersion != sessionVersion
|| CurrentLifetimeMutation(incoming.Guid) != operationVersion)
{
if (tearDownFailure is not null)
@ -675,7 +684,8 @@ public sealed class LiveEntityRuntime
tearDownFailure);
}
var record = new LiveEntityRecord(result.Snapshot);
RuntimeEntityRecord canonical = _directory.AddActive(result.Snapshot);
var record = new LiveEntityRecord(_directory, canonical);
_recordsByGuid.Add(incoming.Guid, record);
return new LiveEntityRegistrationResult(
result,
@ -709,17 +719,30 @@ public sealed class LiveEntityRuntime
if (record.WorldEntity is null)
{
uint localId = ReserveLocalEntityId();
WorldEntity entity = factory(localId)
?? throw new InvalidOperationException("A live-entity projection factory returned null.");
if (entity.Id != localId)
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}.");
uint localId = _directory.ClaimLocalId(record.Canonical);
WorldEntity entity;
try
{
entity = factory(localId)
?? throw new InvalidOperationException(
"A live-entity projection factory returned null.");
if (entity.Id != localId)
{
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;
_isRegisteringResources = true;
try
@ -749,7 +772,7 @@ public sealed class LiveEntityRuntime
if (rollbackError is null)
{
_guidByLocalId.Remove(localId);
_directory.ReleaseLocalId(record.Canonical);
record.WorldEntity = null;
throw;
}
@ -1067,14 +1090,21 @@ public sealed class LiveEntityRuntime
"A live entity cannot unregister from inside atomic resource registration.");
}
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
&& (record.DeleteAcceptedForTeardown
|| _isClearing
|| _sessionClearPendingFinalization);
if (!retryingAcceptedDelete)
{
if (!_inbound.TryDelete(delete, isLocalPlayer))
if (!_directory.TryDelete(delete, isLocalPlayer))
return false;
AdvanceLifetimeMutation(delete.Guid);
ParentAttachments.DeleteGeneration(delete.Guid, delete.InstanceSequence);
@ -1226,8 +1256,19 @@ public sealed class LiveEntityRuntime
_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& record.WorldEntity is not null;
public bool TryGetServerGuid(uint localEntityId, out uint serverGuid) =>
_guidByLocalId.TryGetValue(localEntityId, out serverGuid);
public bool TryGetServerGuid(uint localEntityId, out uint 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)
{
@ -1377,7 +1418,7 @@ public sealed class LiveEntityRuntime
// exact record, authority epoch, and expected owners are revalidated
// before commit just like resource/physics-body acquisition.
LiveEntityRecord incarnation = record;
ulong sessionVersion = _sessionLifetimeVersion;
ulong sessionVersion = _directory.SessionLifetimeVersion;
ulong lifetimeVersion = CurrentLifetimeMutation(serverGuid);
PhysicsBody? expectedBody = record.PhysicsBody;
ILiveEntityRemoteMotionRuntime? expectedRuntime = record.RemoteMotionRuntime;
@ -1436,7 +1477,7 @@ public sealed class LiveEntityRuntime
== expectedIndexPresent
&& (!expectedIndexPresent
|| ReferenceEquals(currentIndexedRuntime, expectedIndexedRuntime));
if (_sessionLifetimeVersion != sessionVersion
if (_directory.SessionLifetimeVersion != sessionVersion
|| CurrentLifetimeMutation(serverGuid) != lifetimeVersion
|| !_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? current)
|| !ReferenceEquals(current, incarnation)
@ -1592,11 +1633,11 @@ public sealed class LiveEntityRuntime
}
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)
{
bool applied = _inbound.TryApplyObjDesc(update, out accepted);
bool applied = _directory.TryApplyObjDesc(update, out accepted);
if (applied)
{
RefreshRecord(update.Guid, accepted);
@ -1608,7 +1649,7 @@ public sealed class LiveEntityRuntime
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)
{
RefreshRecord(update.Guid, accepted);
@ -1622,7 +1663,7 @@ public sealed class LiveEntityRuntime
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)
{
RefreshRecord(update.ChildGuid, accepted);
@ -1634,7 +1675,7 @@ public sealed class LiveEntityRuntime
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)
{
RefreshRecord(update.ChildGuid, accepted);
@ -1648,7 +1689,7 @@ public sealed class LiveEntityRuntime
ParentAttachmentRelation relation,
out WorldSession.EntitySpawn accepted)
{
bool committed = _inbound.TryCommitParent(
bool committed = _directory.TryCommitParent(
relation.ChildGuid,
relation.ParentGuid,
relation.ParentLocation,
@ -1683,8 +1724,8 @@ public sealed class LiveEntityRuntime
out WorldSession.EntitySpawn accepted,
out AcceptedPhysicsTimestamps timestamps)
{
bool applied = _inbound.TryApplyMotion(update, retainPayload, out accepted, out timestamps);
if (_inbound.TryGetSnapshot(update.Guid, out WorldSession.EntitySpawn snapshot))
bool applied = _directory.TryApplyMotion(update, retainPayload, out accepted, out timestamps);
if (_directory.TryGetSnapshot(update.Guid, out WorldSession.EntitySpawn snapshot))
RefreshRecord(update.Guid, snapshot);
if (applied
&& retainPayload
@ -1697,7 +1738,7 @@ public sealed class LiveEntityRuntime
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)
{
RefreshRecord(update.Guid, accepted);
@ -1715,7 +1756,7 @@ public sealed class LiveEntityRuntime
out WorldSession.EntitySpawn accepted,
out RetailPhysicsStateTransition transition)
{
bool applied = _inbound.TryApplyState(update, out accepted);
bool applied = _directory.TryApplyState(update, out accepted);
transition = default;
if (applied)
{
@ -1757,7 +1798,7 @@ public sealed class LiveEntityRuntime
&& (beforePosition.FullCellId == 0
|| !beforePosition.IsSpatiallyProjected
|| !beforePosition.IsSpatiallyVisible);
bool known = _inbound.TryApplyPosition(
bool known = _directory.TryApplyPosition(
update,
isLocalPlayer,
forcePositionRotation,
@ -1765,7 +1806,7 @@ public sealed class LiveEntityRuntime
out disposition,
out accepted,
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;
if (disposition is PositionTimestampDisposition.Apply)
@ -1792,7 +1833,7 @@ public sealed class LiveEntityRuntime
}
public bool IsFreshTeleportStart(uint localPlayerGuid, ushort teleportSequence) =>
_inbound.IsFreshTeleportStart(localPlayerGuid, teleportSequence);
_directory.IsFreshTeleportStart(localPlayerGuid, teleportSequence);
/// <summary>
/// Retail <c>CPhysicsObj::update_object</c> (<c>0x00515D40</c>) runs root
@ -2305,8 +2346,7 @@ public sealed class LiveEntityRuntime
_isClearing = true;
_sessionClearPendingFinalization = true;
_sessionLifetimeVersion++;
_lifetimeMutationVersionByGuid.Clear();
_directory.BeginSessionClear();
List<Exception>? failures = null;
try
{
@ -2319,11 +2359,16 @@ public sealed class LiveEntityRuntime
RetainTeardownRecord(record);
}
ParentAttachments.Clear();
_inbound.Clear();
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
// unregistered by its caller. Leave it queued; the outer
// teardown's ReleaseTeardownRecord finalizes the session once
@ -2369,6 +2414,14 @@ public sealed class LiveEntityRuntime
List<Exception>? failures = null;
foreach (LiveEntityRecord record in _teardownRecords.Values.ToArray())
{
if (!_teardownRecords.TryGetValue(
record.Canonical,
out LiveEntityRecord? retained)
|| !ReferenceEquals(retained, record))
{
continue;
}
if (record.TeardownInProgress)
continue;
@ -2382,7 +2435,7 @@ public sealed class LiveEntityRuntime
completed++;
if (!_recordsByGuid.ContainsKey(record.ServerGuid)
&& !HasPendingTeardown(record.ServerGuid)
&& !_inbound.TryGetSnapshot(record.ServerGuid, out _))
&& !_directory.TryGetSnapshot(record.ServerGuid, out _))
{
_spatial.ForgetLiveEntity(record.ServerGuid);
}
@ -2421,14 +2474,10 @@ public sealed class LiveEntityRuntime
default);
private ulong AdvanceLifetimeMutation(uint serverGuid)
{
ulong next = _lifetimeMutationVersionByGuid.GetValueOrDefault(serverGuid) + 1UL;
_lifetimeMutationVersionByGuid[serverGuid] = next;
return next;
}
=> _directory.AdvanceLifetimeMutation(serverGuid);
private ulong CurrentLifetimeMutation(uint serverGuid) =>
_lifetimeMutationVersionByGuid.GetValueOrDefault(serverGuid);
_directory.CurrentLifetimeMutation(serverGuid);
private bool IsCurrentProjectionOperation(
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)
{
// GpuWorldState serializes re-entrant visibility callbacks. A callback
@ -2688,19 +2720,18 @@ public sealed class LiveEntityRuntime
_visibleWorldEntitiesByGuid.Remove(record.ServerGuid);
}
private static (uint Guid, ushort Generation) TeardownKey(LiveEntityRecord record) =>
(record.ServerGuid, record.Generation);
private void RetainTeardownRecord(LiveEntityRecord record)
{
var key = TeardownKey(record);
if (_teardownRecords.TryGetValue(key, out LiveEntityRecord? retained)
_directory.RetainTeardown(record.Canonical);
if (_teardownRecords.TryGetValue(
record.Canonical,
out LiveEntityRecord? retained)
&& !ReferenceEquals(retained, record))
{
throw new InvalidOperationException(
$"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
&& _visibleWorldEntitiesByGuid.TryGetValue(
record.ServerGuid,
@ -2713,45 +2744,36 @@ public sealed class LiveEntityRuntime
private void ReleaseTeardownRecord(LiveEntityRecord record)
{
var key = TeardownKey(record);
if (_teardownRecords.TryGetValue(key, out LiveEntityRecord? retained)
if (_teardownRecords.TryGetValue(
record.Canonical,
out LiveEntityRecord? retained)
&& ReferenceEquals(retained, record))
{
_teardownRecords.Remove(key);
_teardownRecords.Remove(record.Canonical);
}
_directory.ReleaseTeardown(record.Canonical);
CompleteSessionClearIfConverged();
}
private bool HasPendingTeardown(uint serverGuid)
{
foreach ((uint Guid, ushort Generation) key in _teardownRecords.Keys)
{
if (key.Guid == serverGuid)
return true;
}
return false;
}
private bool HasPendingTeardown(uint serverGuid) =>
_directory.HasPendingTeardown(serverGuid);
private void CompleteSessionClearIfConverged()
{
if (!_sessionClearPendingFinalization
|| _recordsByGuid.Count != 0
|| _teardownRecords.Count != 0)
|| !_directory.CompleteSessionClearIfConverged())
{
return;
}
_materializedWorldEntitiesByGuid.Clear();
_visibleWorldEntitiesByGuid.Clear();
_guidByLocalId.Clear();
_animationsByLocalId.Clear();
_remoteMotionByGuid.Clear();
_spatialAnimationsByLocalId.Clear();
_spatialRemoteMotionByGuid.Clear();
_spatialProjectilesByGuid.Clear();
_spatialRootObjectsByGuid.Clear();
ParentAttachments.Clear();
_inbound.Clear();
_spatial.ClearLiveEntityLifetimeState();
_sessionClearPendingFinalization = false;
}
@ -2815,7 +2837,7 @@ public sealed class LiveEntityRuntime
if (record.WorldEntity is { } finalizedEntity)
{
_guidByLocalId.Remove(finalizedEntity.Id);
_directory.ReleaseLocalId(record.Canonical);
if (_materializedWorldEntitiesByGuid.TryGetValue(
record.ServerGuid,
out WorldEntity? materialized)
@ -2864,4 +2886,80 @@ public sealed class LiveEntityRuntime
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;
}
}