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