diff --git a/src/AcDream.App/World/LiveEntityRuntime.cs b/src/AcDream.App/World/LiveEntityRuntime.cs
index ced3ef7b..3a2c3b23 100644
--- a/src/AcDream.App/World/LiveEntityRuntime.cs
+++ b/src/AcDream.App/World/LiveEntityRuntime.cs
@@ -209,48 +209,78 @@ public sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceLif
}
///
-/// 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 .
+/// 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.
///
public sealed class LiveEntityRecord
{
- private readonly Queue _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; }
+ ///
+ /// 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.
+ ///
+ 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);
+ }
///
/// Incarnation-stable retail CPhysicsObj::update_time owner. It
/// survives animation and MovementManager attachment/replacement, spatial
/// rebucketing, and appearance rehydration, and dies with this record.
///
- public RetailObjectQuantumClock ObjectClock { get; } = new();
+ public RetailObjectQuantumClock ObjectClock => Canonical.ObjectClock;
///
/// Changes whenever this record leaves or re-enters the ordinary-object
/// workset. Unlike , 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.
///
- internal ulong ObjectClockEpoch { get; private set; }
+ internal ulong ObjectClockEpoch => Canonical.ObjectClockEpoch;
///
/// True after this incarnation has successfully constructed its retail
/// CPartArray. 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.
///
- 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.
///
- 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;
///
/// 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.
///
- internal ulong CreateIntegrationVersion { get; private set; } = 1UL;
+ internal ulong CreateIntegrationVersion => Canonical.CreateIntegrationVersion;
public bool WorldSpawnPublished { get; internal set; }
///
/// 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);
///
/// 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);
///
-/// Update-thread owner of live server-object identity, accepted state, runtime
-/// components, and logical teardown. 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
+/// . Runtime owns server GUIDs,
+/// incarnations, accepted snapshots, timestamps, tombstones, and local IDs;
+/// this host owns graphical components and retryable projection teardown.
+/// is a spatial projection only; moving between
+/// loaded and pending buckets never replays a create-time renderer, script,
+/// animation, or effect action.
///
///
/// Retail keeps one object per accepted INSTANCE_TS: same-generation
@@ -454,23 +457,31 @@ public readonly record struct LiveEntityRegistrationResult(
///
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 _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 _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
_teardownRecords = new();
private readonly Dictionary _materializedWorldEntitiesByGuid = new();
private readonly Dictionary _visibleWorldEntitiesByGuid = new();
- private readonly Dictionary _guidByLocalId = new();
private readonly Dictionary _animationsByLocalId = new();
private readonly Dictionary _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 _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 Records => _recordsByGuid.Values;
+ public int Count => _directory.Count;
+ public int PendingTeardownCount => _directory.PendingTeardownCount;
+ public int MaterializedCount => _directory.ClaimedLocalIdCount;
+ public IReadOnlyCollection Records => _activeRecords.Values;
///
/// 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
///
public IReadOnlyDictionary WorldEntities =>
_visibleWorldEntitiesByGuid;
- public IReadOnlyDictionary Snapshots => _inbound.Snapshots;
+ public IReadOnlyDictionary Snapshots => _directory.Snapshots;
public IReadOnlyDictionary AnimationRuntimes => _animationsByLocalId;
public IReadOnlyDictionary RemoteMotionRuntimes => _remoteMotionByGuid;
public IReadOnlyDictionary SpatialAnimationRuntimes =>
@@ -569,7 +574,7 @@ public sealed class LiveEntityRuntime
_spatialProjectilesByGuid;
internal IReadOnlyDictionary SpatialRootObjects =>
_spatialRootObjectsByGuid;
- public ParentAttachmentState ParentAttachments { get; } = new();
+ public ParentAttachmentState ParentAttachments => _directory.ParentAttachments;
///
/// 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);
///
/// Retail CPhysicsObj::update_object (0x00515D40) runs root
@@ -2305,8 +2346,7 @@ public sealed class LiveEntityRuntime
_isClearing = true;
_sessionClearPendingFinalization = true;
- _sessionLifetimeVersion++;
- _lifetimeMutationVersionByGuid.Clear();
+ _directory.BeginSessionClear();
List? 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? 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;
}
}
+
+ ///
+ /// 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.
+ ///
+ private sealed class ActiveRecordView
+ {
+ private readonly RuntimeEntityDirectory _directory;
+ private readonly Dictionary _sidecars;
+
+ public ActiveRecordView(
+ RuntimeEntityDirectory directory,
+ Dictionary sidecars)
+ {
+ _directory = directory;
+ _sidecars = sidecars;
+ }
+
+ public int Count => _directory.Count;
+ public IReadOnlyCollection 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;
+ }
+ }
}
diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs b/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs
new file mode 100644
index 00000000..41501a40
--- /dev/null
+++ b/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs
@@ -0,0 +1,428 @@
+using AcDream.Core.Net;
+using AcDream.Core.Physics;
+
+namespace AcDream.Runtime.Entities;
+
+///
+/// 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.
+///
+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 _activeByGuid = new();
+ private readonly Dictionary<(uint Guid, ushort Incarnation), RuntimeEntityRecord>
+ _teardownByIncarnation = new();
+ private readonly Dictionary _byLocalId = new();
+ private readonly Dictionary _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 ActiveRecords => _activeByGuid.Values;
+ public IReadOnlyCollection TeardownRecords =>
+ _teardownByIncarnation.Values;
+ public IReadOnlyDictionary 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.");
+ }
+ }
+}
diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityRecord.cs b/src/AcDream.Runtime/Entities/RuntimeEntityRecord.cs
new file mode 100644
index 00000000..d4c60879
--- /dev/null
+++ b/src/AcDream.Runtime/Entities/RuntimeEntityRecord.cs
@@ -0,0 +1,160 @@
+using AcDream.Core.Net;
+using AcDream.Core.Physics;
+
+namespace AcDream.Runtime.Entities;
+
+///
+/// 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.
+///
+public readonly record struct RuntimeEntityKey(
+ uint LocalEntityId,
+ ushort Incarnation);
+
+///
+/// Presentation-free canonical state for one accepted server-object
+/// incarnation. App projection, animation, effects, visibility, and hydration
+/// state must never be stored here.
+///
+public sealed class RuntimeEntityRecord
+{
+ private readonly Queue _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; }
+
+ ///
+ /// Incarnation-stable retail CPhysicsObj::update_time owner.
+ ///
+ public RetailObjectQuantumClock ObjectClock { get; } = new();
+
+ public ulong ObjectClockEpoch { get; private set; }
+
+ ///
+ /// True after this incarnation has successfully constructed its retail
+ /// CPartArray. This is a simulation/lifetime fact, not a renderer
+ /// object reference.
+ ///
+ 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;
+ }
+}
diff --git a/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs b/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs
new file mode 100644
index 00000000..8190d4d5
--- /dev/null
+++ b/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs
@@ -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;
+ }
+}
diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityDirectoryTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityDirectoryTests.cs
new file mode 100644
index 00000000..19b82656
--- /dev/null
+++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityDirectoryTests.cs
@@ -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(
+ () => 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(
+ () => new RuntimeEntityDirectory(
+ RuntimeEntityDirectory.FirstLocalEntityId - 1u));
+ Assert.Throws(
+ () => 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(),
+ Array.Empty(),
+ Array.Empty(),
+ null,
+ null,
+ "fixture",
+ null,
+ null,
+ 0x09000001u,
+ PhysicsState: 0x408u,
+ InstanceSequence: instance,
+ MovementSequence: 1,
+ ServerControlSequence: 1,
+ PositionSequence: 1,
+ Physics: physics);
+ }
+}