Campaign P remaining-physics-divergence, placement cutover slice C3c
(docs/plans/2026-08-02-placement-cutover.md). Both production hosts now
register every initial Create through the residence + continuation-
executor + first-entry-conductor machinery (C0-C3b):
- Graphical (route 1): RegisterEntityWithInitialResidence at Create; the
shared RuntimeFirstEntryDriveController pumps both conductors from the
placement-receipt flow; MaterializeProjection and RebucketLiveEntity
are presentation-only while a residence is ACTIVE (ExecutorCompleted is
the presentation-binding receipt); post-residence entities take the
full legacy path including the prepare_to_enter_world clock edges.
PlayerModeController attaches presentation to the Runtime-published
controller; its legacy resolve/step-heights/host-construction path is
deleted; presentation-only rollback (retail has no entry-flow rollback).
- Headless (route 8): OnSpawned registers with residence when a drive
exists; content-less sessions keep the pre-flip direct registration;
SynchronizeLocalPlayer/CreateController/ApplySetupStepHeights deleted;
prepared-collision read failure is a typed AwaitingCollisionSource
retry; far remotes outside the service window complete celless.
- RuntimeLocalPlayerMovementState.Controller setter sealed internal; all
controller mutation flows through the publication lifecycle.
Fix slices landed within this cutover, each dual-gated:
- F1: live movement-stat/server-physics application routed through the
Runtime ownership seam (post-logout ingest crash on the retired
controller eliminated; RuntimeMovementSkillProjection deleted).
- F2: login activation wedge - collision-admission prefix gate factored
out of the seal (reentrant-commit RejectedAuthority), rearm generation
identity corrected, PlayerModeAutoEntry requires the Runtime-published
controller (world reveal can no longer seal unmaterialized).
- F3: landblock-prefix 0-sentinel replaced by explicit absent-id guards;
map-corner landblocks (grid row/col 0) fully legal through admission,
park/rearm/retire, quiescence, and outdoor shadow seeds.
- F5: local-player first-entry ground contact seeded by the shared
SpawnPlacementSettler (moved App->Core) at FinalizeActivation - the
retail first-gravity-frame touch (enter_world 0x00516170 carries no
seed); the legacy unconditional force-seed is overwritten by a real
floor-found contact; airborne spawns stay airborne; outbound contact
bit verified end-to-end. Fixes the standing-cast 'You can't do that
while in the air!' rejections.
- R1 (dual-review round): login constraint leash armed at the committed
placement (HandleReceivedPosition 0x00453FD0 analog); register rows
AD-61 (settle-timing compression now covering the local player) and
AD-42 (repointed off the deleted resolve split) in this commit;
residence-conversion owner API; wire-landblock guards; drive-pending
ledger in IsConverged; route attach/detach latch; executor-drain drift
model documented + source-pinned.
Gates: Runtime 1,003, App 4,039/3 skips, Headless 79, complete solution
10,816/0 failed/4 skips (Release, -m:1); connected lifecycle/reconnect
gate PASS (logs/connected-world-gate-20260802-175401; graceful exits,
world-visible, zero airborne rejections). The nine-stop soak remains red
for the pre-existing 6b28ff99 whole-world collision-clone throughput
regression (attributed with evidence; scheduled as its own slice before
C5). Dual Opus reviews (retail-conformance + adversarial): delta PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3504 lines
132 KiB
C#
3504 lines
132 KiB
C#
using AcDream.App.Streaming;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.World;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Physics;
|
|
using System.Numerics;
|
|
using System.Runtime.ExceptionServices;
|
|
|
|
namespace AcDream.App.World;
|
|
|
|
/// <summary>
|
|
/// Animation state owned by a live object. The concrete App animation runtime
|
|
/// implements this seam so identity storage does not depend on a render backend.
|
|
/// </summary>
|
|
public interface ILiveEntityAnimationRuntime
|
|
{
|
|
WorldEntity Entity { get; }
|
|
uint CurrentMotion { get; }
|
|
}
|
|
|
|
/// <summary>Marker for the DAT-driven effect profile added by the effect phase.</summary>
|
|
public interface ILiveEntityEffectProfile { }
|
|
|
|
public enum LiveEntityProjectionKind
|
|
{
|
|
World,
|
|
Attached,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Selects whether App may publish a newly constructed sidecar immediately or
|
|
/// must keep it non-spatial until the canonical Runtime SetPosition receipt is
|
|
/// projected. The latter is the production CreateObject path; the former is a
|
|
/// temporary compatibility seam for callers not yet cut over in Slice 4B2.
|
|
/// </summary>
|
|
internal enum LiveEntityMaterializationResidence
|
|
{
|
|
LegacyImmediate,
|
|
AwaitRuntimePlacement,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logical-resource seam coordinated by <see cref="LiveEntityRuntime"/>.
|
|
/// Spatial bucketing is deliberately absent: registering or removing meshes,
|
|
/// scripts, and effects is a logical-lifetime operation, not a landblock move.
|
|
/// </summary>
|
|
public interface ILiveEntityResourceLifecycle
|
|
{
|
|
void Register(WorldEntity entity);
|
|
void Unregister(WorldEntity entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exact-incarnation App component cleanup invoked by the canonical runtime
|
|
/// after it has retired the active identity. Mesh/script resource lifetime is
|
|
/// owned separately by <see cref="ILiveEntityResourceLifecycle"/>.
|
|
/// </summary>
|
|
internal interface ILiveEntityRuntimeComponentLifecycle
|
|
{
|
|
void TearDown(LiveEntityRecord record);
|
|
}
|
|
|
|
internal sealed class NullLiveEntityRuntimeComponentLifecycle :
|
|
ILiveEntityRuntimeComponentLifecycle
|
|
{
|
|
public static NullLiveEntityRuntimeComponentLifecycle Instance { get; } = new();
|
|
private NullLiveEntityRuntimeComponentLifecycle() { }
|
|
public void TearDown(LiveEntityRecord record) { }
|
|
}
|
|
|
|
internal sealed class DelegateLiveEntityRuntimeComponentLifecycle(
|
|
Action<LiveEntityRecord> tearDown) : ILiveEntityRuntimeComponentLifecycle
|
|
{
|
|
private readonly Action<LiveEntityRecord> _tearDown =
|
|
tearDown ?? throw new ArgumentNullException(nameof(tearDown));
|
|
|
|
public void TearDown(LiveEntityRecord record) => _tearDown(record);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Composition bridge for the runtime-to-hydration construction cycle. It is
|
|
/// deliberately fail-fast and single-assignment: a live session must not start
|
|
/// before the exact component owner is bound, and ownership cannot be replaced
|
|
/// during a session.
|
|
/// </summary>
|
|
internal sealed class DeferredLiveEntityRuntimeComponentLifecycle :
|
|
ILiveEntityRuntimeComponentLifecycle
|
|
{
|
|
private ILiveEntityRuntimeComponentLifecycle? _inner;
|
|
|
|
public bool IsBound => Volatile.Read(ref _inner) is not null;
|
|
|
|
public void Bind(ILiveEntityRuntimeComponentLifecycle lifecycle)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(lifecycle);
|
|
if (ReferenceEquals(lifecycle, this))
|
|
throw new ArgumentException("A lifecycle bridge cannot bind to itself.", nameof(lifecycle));
|
|
if (Interlocked.CompareExchange(ref _inner, lifecycle, null) is not null)
|
|
throw new InvalidOperationException("The live-entity runtime component lifecycle is already bound.");
|
|
}
|
|
|
|
public IDisposable BindOwned(ILiveEntityRuntimeComponentLifecycle lifecycle)
|
|
{
|
|
Bind(lifecycle);
|
|
return new Binding(this, lifecycle);
|
|
}
|
|
|
|
private void Unbind(ILiveEntityRuntimeComponentLifecycle expected)
|
|
{
|
|
_ = Interlocked.CompareExchange(ref _inner, null, expected);
|
|
}
|
|
|
|
public void TearDown(LiveEntityRecord record)
|
|
{
|
|
ILiveEntityRuntimeComponentLifecycle lifecycle = Volatile.Read(ref _inner)
|
|
?? throw new InvalidOperationException(
|
|
"The live-entity runtime component lifecycle has not been bound.");
|
|
lifecycle.TearDown(record);
|
|
}
|
|
|
|
private sealed class Binding : IDisposable
|
|
{
|
|
private DeferredLiveEntityRuntimeComponentLifecycle? _owner;
|
|
private readonly ILiveEntityRuntimeComponentLifecycle _expected;
|
|
|
|
public Binding(
|
|
DeferredLiveEntityRuntimeComponentLifecycle owner,
|
|
ILiveEntityRuntimeComponentLifecycle expected)
|
|
{
|
|
_owner = owner;
|
|
_expected = expected;
|
|
}
|
|
|
|
public void Dispose() =>
|
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
|
}
|
|
}
|
|
|
|
/// <summary>Delegate adapter used by the App composition root.</summary>
|
|
public sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceLifecycle
|
|
{
|
|
private readonly Action<WorldEntity> _register;
|
|
private readonly Action<WorldEntity> _unregister;
|
|
|
|
public DelegateLiveEntityResourceLifecycle(
|
|
Action<WorldEntity> register,
|
|
Action<WorldEntity> unregister)
|
|
{
|
|
_register = register ?? throw new ArgumentNullException(nameof(register));
|
|
_unregister = unregister ?? throw new ArgumentNullException(nameof(unregister));
|
|
}
|
|
|
|
public void Register(WorldEntity entity) => _register(entity);
|
|
public void Unregister(WorldEntity entity) => _unregister(entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// App projection sidecar for one canonical <see cref="RuntimeEntityRecord"/>.
|
|
/// The canonical record survives loaded/pending landblock movement and
|
|
/// temporary loss of its render bucket; this sidecar retains only graphical
|
|
/// components, visibility, hydration, and teardown progress.
|
|
/// </summary>
|
|
public sealed class LiveEntityRecord
|
|
{
|
|
private readonly RuntimeEntityDirectory _directory;
|
|
|
|
internal LiveEntityRecord(
|
|
RuntimeEntityDirectory directory,
|
|
RuntimeEntityRecord canonical)
|
|
{
|
|
_directory = directory ?? throw new ArgumentNullException(nameof(directory));
|
|
Canonical = canonical ?? throw new ArgumentNullException(nameof(canonical));
|
|
}
|
|
|
|
internal RuntimeEntityRecord Canonical { get; }
|
|
internal RuntimeEntityKey? ProjectionKey { get; set; }
|
|
public uint ServerGuid => Canonical.ServerGuid;
|
|
public ushort Generation => Canonical.Incarnation;
|
|
public WorldSession.EntitySpawn Snapshot
|
|
{
|
|
get => Canonical.Snapshot;
|
|
internal set => _directory.RefreshSnapshot(Canonical, value);
|
|
}
|
|
public WorldEntity? WorldEntity { get; internal set; }
|
|
public uint? LocalEntityId => Canonical.LocalEntityId ?? WorldEntity?.Id;
|
|
public uint FullCellId
|
|
{
|
|
get => Canonical.FullCellId;
|
|
internal set => _directory.SetFullCell(
|
|
Canonical,
|
|
value,
|
|
Canonical.CanonicalLandblockId);
|
|
}
|
|
public uint CanonicalLandblockId
|
|
{
|
|
get => Canonical.CanonicalLandblockId;
|
|
internal set => _directory.SetFullCell(
|
|
Canonical,
|
|
Canonical.FullCellId,
|
|
value);
|
|
}
|
|
public uint RawPhysicsState => Canonical.RawPhysicsState;
|
|
public PhysicsStateFlags FinalPhysicsState
|
|
{
|
|
get => Canonical.FinalPhysicsState;
|
|
internal set => _directory.SetFinalPhysicsState(Canonical, value);
|
|
}
|
|
/// <summary>
|
|
/// Incarnation-stable retail <c>CPhysicsObj::update_time</c> owner. It
|
|
/// survives animation and MovementManager attachment/replacement, spatial
|
|
/// rebucketing, and appearance rehydration, and dies with this record.
|
|
/// </summary>
|
|
public RetailObjectQuantumClock ObjectClock => Canonical.ObjectClock;
|
|
/// <summary>
|
|
/// Changes whenever this record leaves or re-enters the ordinary-object
|
|
/// workset. Unlike <see cref="ProjectionMutationVersion"/>, loaded-to-loaded
|
|
/// rebucketing does not change this token. A scheduler can therefore stop a
|
|
/// catch-up batch that crossed a leave/enter boundary without rejecting an
|
|
/// ordinary same-frame cell move.
|
|
/// </summary>
|
|
internal ulong ObjectClockEpoch => Canonical.ObjectClockEpoch;
|
|
/// <summary>
|
|
/// True after this incarnation has successfully constructed its retail
|
|
/// <c>CPartArray</c>. This is an object-lifetime fact, not an animation-
|
|
/// runtime proxy: static poses and objects without a MotionTable still
|
|
/// own a PartArray and participate in the 96-unit Active gate.
|
|
/// </summary>
|
|
public bool HasPartArray
|
|
{
|
|
get => Canonical.HasPartArray;
|
|
internal set => _directory.SetHasPartArray(Canonical, value);
|
|
}
|
|
public PhysicsBody? PhysicsBody
|
|
{
|
|
get => Canonical.PhysicsBody;
|
|
}
|
|
internal bool PhysicsBodyAcquisitionInProgress
|
|
{
|
|
get => Canonical.PhysicsBodyAcquisitionInProgress;
|
|
}
|
|
public ILiveEntityAnimationRuntime? AnimationRuntime { get; internal set; }
|
|
public IRuntimeRemoteMotion? RemoteMotionRuntime
|
|
{
|
|
get => Canonical.RemoteMotion;
|
|
}
|
|
internal bool RemoteMotionBindingInProgress
|
|
{
|
|
get => Canonical.RemoteMotionBindingInProgress;
|
|
}
|
|
public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost
|
|
{
|
|
get => Canonical.PhysicsHost;
|
|
}
|
|
internal bool RequiresRemotePlacementRuntime
|
|
{
|
|
get => Canonical.RequiresRemotePlacementRuntime;
|
|
}
|
|
public IRuntimeProjectile? ProjectileRuntime => Canonical.Projectile;
|
|
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
|
|
public bool ResourcesRegistered { get; internal set; }
|
|
internal LiveEntityMaterializationResidence? MaterializationResidence
|
|
{ get; set; }
|
|
public bool IsSpatiallyProjected { get; internal set; }
|
|
public bool IsSpatiallyVisible { get; internal set; }
|
|
internal ulong ProjectionMutationVersion { get; set; }
|
|
/// <summary>
|
|
/// Monotonic App operation tokens for accepted wire authority. They let a
|
|
/// handler detect a newer same-incarnation packet delivered re-entrantly
|
|
/// while an older packet is crossing presentation or spatial callbacks.
|
|
/// </summary>
|
|
internal ulong PositionAuthorityVersion => Canonical.PositionAuthorityVersion;
|
|
internal ulong StateAuthorityVersion => Canonical.StateAuthorityVersion;
|
|
internal ulong VectorAuthorityVersion => Canonical.VectorAuthorityVersion;
|
|
internal ulong VelocityAuthorityVersion => Canonical.VelocityAuthorityVersion;
|
|
internal ulong MovementAuthorityVersion => Canonical.MovementAuthorityVersion;
|
|
internal ulong ObjDescAuthorityVersion => Canonical.ObjDescAuthorityVersion;
|
|
/// <summary>
|
|
/// Advances for every accepted CreateObject affecting this incarnation,
|
|
/// including a fresher same-INSTANCE_TS retransmit. App integration uses
|
|
/// it to distinguish two transactions that intentionally share identity.
|
|
/// </summary>
|
|
internal ulong CreateIntegrationVersion => Canonical.CreateIntegrationVersion;
|
|
public bool WorldSpawnPublished { get; internal set; }
|
|
/// <summary>
|
|
/// True only after the incarnation's projection, collision/animation
|
|
/// components, effect owner, state presentation, and pending-effect replay
|
|
/// have crossed the initial ready barrier. A non-null
|
|
/// <see cref="WorldEntity"/> alone is not sufficient: failures after
|
|
/// resource registration retain the same projection for exact retry.
|
|
/// </summary>
|
|
public bool InitialHydrationCompleted { get; internal set; }
|
|
/// <summary>
|
|
/// A fresher same-incarnation CreateObject interrupted projection after
|
|
/// logical ownership had already been established. The retained entity
|
|
/// must replay the newest canonical appearance/current snapshot/animation
|
|
/// before ordinary rebucketing can resume. This obligation deliberately
|
|
/// survives a failed recovery attempt.
|
|
/// </summary>
|
|
internal bool CreateProjectionSynchronizationPending { get; set; }
|
|
internal bool ProjectionHydrationInProgress { get; set; }
|
|
internal ulong ProjectionHydrationCreateVersion { get; set; }
|
|
internal bool ProjectionHydrationRetryRequested { get; set; }
|
|
/// <summary>
|
|
/// A renderer mesh transaction can be re-entered by a newer accepted
|
|
/// ObjDesc. Keep that independent projection obligation on the exact
|
|
/// incarnation until the newest visual description publishes.
|
|
/// </summary>
|
|
internal bool AppearanceProjectionSynchronizationPending { get; set; }
|
|
internal bool AppearanceHydrationInProgress { get; set; }
|
|
internal ulong AppearanceHydrationVersion { get; set; }
|
|
internal bool AppearanceHydrationRetryRequested { get; set; }
|
|
public LiveEntityProjectionKind ProjectionKind { get; internal set; }
|
|
internal bool RuntimeComponentsTeardownCompleted { get; set; }
|
|
internal LiveEntityTeardownPlan? RuntimeComponentTeardownPlan { get; set; }
|
|
internal bool SpatialProjectionTeardownCompleted { get; set; }
|
|
internal bool TeardownInProgress { get; set; }
|
|
internal bool DeleteAcceptedForTeardown
|
|
{
|
|
get => Canonical.DeleteAcceptedForTeardown;
|
|
set => _directory.SetDeleteAcceptedForTeardown(Canonical, value);
|
|
}
|
|
|
|
internal bool TryDequeueStateTransition(out RetailPhysicsStateTransition transition) =>
|
|
_directory.TryDequeueStateTransition(Canonical, out transition);
|
|
|
|
internal void SuspendObjectClock() => _directory.SuspendObjectClock(Canonical);
|
|
|
|
internal void ResetObjectClockForEnterWorld(bool isStatic) =>
|
|
_directory.ResetObjectClockForEnterWorld(Canonical, isStatic);
|
|
|
|
internal RetailPhysicsStateTransition ApplyRawPhysicsState(uint rawState) =>
|
|
_directory.ApplyRawPhysicsState(Canonical, rawState);
|
|
|
|
internal void NoteObjDescProjectionSynchronization()
|
|
{
|
|
AppearanceProjectionSynchronizationPending = true;
|
|
if (AppearanceHydrationInProgress)
|
|
AppearanceHydrationRetryRequested = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cell used when a streamed landblock asks live objects to restore their
|
|
/// spatial projection. Once materialized, the canonical runtime cell wins;
|
|
/// the retained server snapshot is only a first-materialization source.
|
|
/// </summary>
|
|
public uint ProjectionCellId => WorldEntity is not null
|
|
? FullCellId
|
|
: Snapshot.Position?.LandblockId ?? FullCellId;
|
|
|
|
internal void RefreshDerivedState(bool refreshPosition = true) =>
|
|
_directory.RefreshSnapshot(Canonical, Snapshot, refreshPosition);
|
|
}
|
|
|
|
public readonly record struct LiveEntityRegistrationResult(
|
|
InboundCreateResult Inbound,
|
|
RuntimeEntityRecord? Canonical,
|
|
LiveEntityRecord? Projection,
|
|
bool LogicalRegistrationCreated,
|
|
bool ReplacedExistingGeneration,
|
|
Exception? PriorGenerationCleanupFailure = null);
|
|
|
|
/// <summary>
|
|
/// Update-thread App projection/lifecycle host over the canonical
|
|
/// <see cref="RuntimeEntityDirectory"/>. Runtime owns server GUIDs,
|
|
/// incarnations, accepted snapshots, timestamps, tombstones, and local IDs;
|
|
/// this host owns graphical components and retryable projection teardown.
|
|
/// <see cref="GpuWorldState"/> is a spatial projection only; moving between
|
|
/// loaded and pending buckets never replays a create-time renderer, script,
|
|
/// animation, or effect action.
|
|
///
|
|
/// <para>
|
|
/// Retail keeps one object per accepted INSTANCE_TS: same-generation
|
|
/// CreateObject branches mutate it, Pickup leaves world without destruction,
|
|
/// and Delete/instance replacement end it. See SmartBox CreateObject/event
|
|
/// pseudocode in <c>2026-07-13-retail-projectile-vfx-pseudocode.md</c>,
|
|
/// <c>CPhysicsObj::set_description</c> (0x00514F40),
|
|
/// <c>CPhysicsObj::change_cell</c> (0x00513390), and
|
|
/// <c>CPhysicsObj::exit_world</c> (0x00514E60).
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|
{
|
|
public const uint FirstLiveEntityId = RuntimeEntityDirectory.FirstLocalEntityId;
|
|
public const uint LastLiveEntityId = RuntimeEntityDirectory.LastLocalEntityId;
|
|
|
|
private readonly GpuWorldState _spatial;
|
|
private readonly ILiveEntityResourceLifecycle _resources;
|
|
private readonly ILiveEntityRuntimeComponentLifecycle _runtimeComponentLifecycle;
|
|
private readonly RuntimeEntityObjectLifetime _entityObjects;
|
|
private readonly RuntimeEntityDirectory _directory;
|
|
private readonly RuntimePhysicsState _physics;
|
|
private readonly LiveEntityProjectionStore _projections;
|
|
// Logical components survive retail's 25-second leave-visibility lifetime.
|
|
// Frame loops must not scan those retained owners. These component-specific
|
|
// indexes contain only records with a loaded, non-cellless spatial
|
|
// projection. Hidden and Frozen remain members: their state controls what
|
|
// each retail update path advances after the O(active) lookup.
|
|
private readonly Dictionary<RuntimeEntityKey, ILiveEntityAnimationRuntime> _spatialAnimations = new();
|
|
private readonly List<RuntimeEntityRecord> _spatialRootCanonicalScratch = new();
|
|
private readonly List<RuntimeEntityRecord> _spatialRemoteCanonicalScratch = new();
|
|
// Runtime placement receipts are presentation observations of an already
|
|
// committed canonical SetPosition transaction. GpuWorldState still emits
|
|
// its ordinary visibility callback while that observation rebuckets or
|
|
// withdraws a sidecar, so pin the exact incarnation while the callback is
|
|
// in flight. A depth (rather than a bool) preserves nested/re-entrant
|
|
// projection mutations without ever turning graphical visibility into a
|
|
// Runtime physics/workset mutation.
|
|
private readonly Dictionary<RuntimeEntityKey, int>
|
|
_presentationOnlySpatialMutationDepth = new();
|
|
private bool _isClearing;
|
|
private bool _sessionClearPendingFinalization;
|
|
private bool _isRegisteringResources;
|
|
private int _logicalTeardownDepth;
|
|
private uint _rebucketingGuid;
|
|
|
|
public LiveEntityRuntime(
|
|
GpuWorldState spatial,
|
|
ILiveEntityResourceLifecycle resources,
|
|
RuntimeEntityObjectLifetime entityObjects)
|
|
: this(
|
|
spatial,
|
|
resources,
|
|
NullLiveEntityRuntimeComponentLifecycle.Instance,
|
|
entityObjects)
|
|
{
|
|
}
|
|
|
|
public LiveEntityRuntime(
|
|
GpuWorldState spatial,
|
|
ILiveEntityResourceLifecycle resources,
|
|
Action<LiveEntityRecord> tearDownRuntimeComponents,
|
|
RuntimeEntityObjectLifetime entityObjects)
|
|
: this(
|
|
spatial,
|
|
resources,
|
|
new DelegateLiveEntityRuntimeComponentLifecycle(tearDownRuntimeComponents),
|
|
entityObjects)
|
|
{
|
|
}
|
|
|
|
internal LiveEntityRuntime(
|
|
GpuWorldState spatial,
|
|
ILiveEntityResourceLifecycle resources,
|
|
ILiveEntityRuntimeComponentLifecycle? runtimeComponentLifecycle,
|
|
RuntimeEntityObjectLifetime entityObjects)
|
|
{
|
|
_spatial = spatial ?? throw new ArgumentNullException(nameof(spatial));
|
|
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
|
_runtimeComponentLifecycle = runtimeComponentLifecycle
|
|
?? throw new ArgumentNullException(nameof(runtimeComponentLifecycle));
|
|
_entityObjects = entityObjects
|
|
?? throw new ArgumentNullException(nameof(entityObjects));
|
|
_directory = _entityObjects.Entities;
|
|
_physics = _entityObjects.Physics;
|
|
_projections = new LiveEntityProjectionStore(_directory);
|
|
_spatial.LiveProjectionVisibilityChanged += OnSpatialVisibilityChanged;
|
|
_physics.CellCommitted += OnRuntimePhysicsCellCommitted;
|
|
}
|
|
|
|
public int Count => _directory.Count;
|
|
public int PendingTeardownCount => _directory.PendingTeardownCount;
|
|
public int MaterializedCount => _projections.MaterializedCount;
|
|
public IReadOnlyCollection<LiveEntityRecord> Records => _projections.ActiveRecords;
|
|
public IReadOnlyCollection<LiveEntityRecord> MaterializedRecords =>
|
|
_projections.MaterializedRecords;
|
|
public IReadOnlyCollection<LiveEntityRecord> VisibleRecords =>
|
|
_projections.VisibleRecords;
|
|
internal IReadOnlyCollection<RuntimeEntityRecord> CanonicalRecords =>
|
|
_directory.ActiveRecords;
|
|
internal ulong SessionLifetimeVersion => _directory.SessionLifetimeVersion;
|
|
internal RuntimePhysicsState Physics => _physics;
|
|
public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _directory.Snapshots;
|
|
internal int AnimationRuntimeCount
|
|
{
|
|
get
|
|
{
|
|
int count = 0;
|
|
IReadOnlyList<LiveEntityRecord> records = _projections.ActiveRecords;
|
|
for (int i = 0; i < records.Count; i++)
|
|
{
|
|
if (records[i].AnimationRuntime is not null)
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
}
|
|
internal int SpatialAnimationRuntimeCount => _spatialAnimations.Count;
|
|
internal int SpatialRemoteMotionRuntimeCount => _physics.SpatialRemoteCount;
|
|
internal int SpatialProjectileRuntimeCount => _physics.SpatialProjectileCount;
|
|
internal int SpatialRootObjectCount => _physics.SpatialRootCount;
|
|
public ParentAttachmentState ParentAttachments => _directory.ParentAttachments;
|
|
|
|
internal bool TryGetCanonical(
|
|
uint serverGuid,
|
|
out RuntimeEntityRecord canonical) =>
|
|
_directory.TryGetActive(serverGuid, out canonical);
|
|
|
|
internal bool TryGetProjection(
|
|
RuntimeEntityRecord canonical,
|
|
out LiveEntityRecord record) =>
|
|
_projections.TryGet(canonical, out record);
|
|
|
|
internal bool IsCurrentCanonical(RuntimeEntityRecord canonical) =>
|
|
_directory.IsCurrent(canonical);
|
|
|
|
internal bool IsCurrentCreateIntegration(
|
|
RuntimeEntityRecord canonical,
|
|
ulong expectedCreateIntegrationVersion) =>
|
|
_directory.IsCurrent(canonical)
|
|
&& canonical.CreateIntegrationVersion == expectedCreateIntegrationVersion;
|
|
|
|
internal void SetHasPartArray(
|
|
RuntimeEntityRecord canonical,
|
|
bool value) =>
|
|
_directory.SetHasPartArray(canonical, value);
|
|
|
|
/// <summary>
|
|
/// Raised after a materialized projection enters or leaves the currently
|
|
/// loaded spatial view. Logical resources remain owned by the record.
|
|
/// </summary>
|
|
public event Action<LiveEntityRecord, bool>? ProjectionVisibilityChanged;
|
|
|
|
public LiveEntityRegistrationResult RegisterLiveEntity(
|
|
WorldSession.EntitySpawn incoming,
|
|
bool isLocalPlayer = false)
|
|
{
|
|
if (_isClearing || _sessionClearPendingFinalization || _isRegisteringResources)
|
|
{
|
|
throw new InvalidOperationException(
|
|
_isClearing || _sessionClearPendingFinalization
|
|
? "A live entity cannot register while the session lifetime is clearing."
|
|
: "A live entity cannot register from inside atomic resource registration.");
|
|
}
|
|
|
|
// C3c route-1 flip: every graphical initial Create enters the
|
|
// canonical initial-residence lease. The accepted wire frame stays on
|
|
// the canonical record with FullCell 0 until the authored Runtime
|
|
// SetPosition operation commits; the host's first-entry drive
|
|
// controller walks the conductors from the residence-begin
|
|
// notification.
|
|
RuntimeEntityRegistrationResult registration =
|
|
_entityObjects.RegisterEntityWithInitialResidence(
|
|
incoming,
|
|
isLocalPlayer,
|
|
RetirePriorProjection);
|
|
RuntimeEntityRecord? canonical = registration.Canonical;
|
|
LiveEntityRecord? projection = canonical is null
|
|
? null
|
|
: _projections.GetCurrentOrDefault(canonical.ServerGuid);
|
|
if (projection is not null
|
|
&& ReferenceEquals(projection.Canonical, canonical))
|
|
{
|
|
RefreshSpatialRuntimeIndexes(projection);
|
|
}
|
|
return new LiveEntityRegistrationResult(
|
|
registration.Inbound,
|
|
canonical,
|
|
projection,
|
|
registration.LogicalRegistrationCreated,
|
|
registration.ReplacedExistingGeneration,
|
|
registration.PriorGenerationCleanupFailure);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes logical registration when a renderable projection can be
|
|
/// hydrated. The factory is called at most once for the incarnation.
|
|
/// </summary>
|
|
public WorldEntity? MaterializeLiveEntity(
|
|
uint serverGuid,
|
|
uint fullCellId,
|
|
Func<uint, WorldEntity> factory,
|
|
LiveEntityProjectionKind projectionKind = LiveEntityProjectionKind.World)
|
|
{
|
|
if (!_directory.TryGetActive(
|
|
serverGuid,
|
|
out RuntimeEntityRecord canonical))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return MaterializeLiveEntity(
|
|
canonical,
|
|
fullCellId,
|
|
factory,
|
|
projectionKind,
|
|
initializeProjection: null,
|
|
out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Claims the canonical Runtime local ID, creates its exact-key App
|
|
/// sidecar, and synchronously constructs the projection. No sidecar exists
|
|
/// before this method crosses the local-ID claim boundary.
|
|
/// </summary>
|
|
internal WorldEntity? MaterializeLiveEntity(
|
|
RuntimeEntityRecord expectedCanonical,
|
|
uint fullCellId,
|
|
Func<uint, WorldEntity> factory,
|
|
LiveEntityProjectionKind projectionKind,
|
|
Action<LiveEntityRecord>? initializeProjection,
|
|
out LiveEntityRecord? record,
|
|
LiveEntityMaterializationResidence residence =
|
|
LiveEntityMaterializationResidence.LegacyImmediate)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedCanonical);
|
|
ArgumentNullException.ThrowIfNull(factory);
|
|
if (residence is LiveEntityMaterializationResidence
|
|
.AwaitRuntimePlacement
|
|
&& projectionKind is not LiveEntityProjectionKind.World)
|
|
{
|
|
throw new ArgumentException(
|
|
"Only a top-level world projection can await canonical Runtime placement.",
|
|
nameof(projectionKind));
|
|
}
|
|
record = null;
|
|
if (_isClearing
|
|
|| _sessionClearPendingFinalization
|
|
|| _logicalTeardownDepth != 0
|
|
|| _isRegisteringResources)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"A live entity cannot materialize inside an active logical-lifetime transition.");
|
|
}
|
|
if (!_directory.IsCurrent(expectedCanonical))
|
|
return null;
|
|
|
|
uint serverGuid = expectedCanonical.ServerGuid;
|
|
bool createdSidecar = false;
|
|
if (!_projections.TryGet(expectedCanonical, out record))
|
|
{
|
|
_ = expectedCanonical.LocalEntityId
|
|
?? throw new InvalidOperationException(
|
|
"Runtime must issue entity identity before App projection.");
|
|
try
|
|
{
|
|
record = _projections.AddMaterializing(expectedCanonical);
|
|
createdSidecar = true;
|
|
record.MaterializationResidence = residence;
|
|
initializeProjection?.Invoke(record);
|
|
}
|
|
catch
|
|
{
|
|
if (record is not null)
|
|
{
|
|
_projections.RemoveActive(record);
|
|
record.ProjectionKey = null;
|
|
record = null;
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
else if (record.MaterializationResidence != residence)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Live entity 0x{serverGuid:X8}/{expectedCanonical.Incarnation} "
|
|
+ $"cannot change its materialization residence from "
|
|
+ $"{record.MaterializationResidence} to {residence}.");
|
|
}
|
|
|
|
if (record.WorldEntity is null)
|
|
{
|
|
uint localId = expectedCanonical.LocalEntityId
|
|
?? throw new InvalidOperationException(
|
|
"A materializing App sidecar lost its Runtime local ID.");
|
|
WorldEntity entity;
|
|
try
|
|
{
|
|
entity = factory(localId)
|
|
?? throw new InvalidOperationException(
|
|
"A live-entity projection factory returned null.");
|
|
if (entity.Id != localId)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Live projection id 0x{entity.Id:X8} did not match reserved id 0x{localId:X8}.");
|
|
}
|
|
if (entity.ServerGuid != serverGuid)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Live projection guid 0x{entity.ServerGuid:X8} did not match record 0x{serverGuid:X8}.");
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
if (createdSidecar)
|
|
{
|
|
_projections.RemoveActive(record);
|
|
record.ProjectionKey = null;
|
|
record = null;
|
|
}
|
|
throw;
|
|
}
|
|
|
|
record.WorldEntity = entity;
|
|
// Runtime-placement residence is sticky for this incarnation and
|
|
// must suppress draw before arbitrary resource callbacks observe
|
|
// or re-enter the sidecar.
|
|
RefreshPresentation(record);
|
|
_isRegisteringResources = true;
|
|
try
|
|
{
|
|
// Registration may span several App owners. Until the whole
|
|
// edge returns, Unregister remains a required rollback
|
|
// obligation; a failed rollback retains this identity as a
|
|
// teardown tombstone instead of making partial owners
|
|
// unreachable.
|
|
record.ResourcesRegistered = true;
|
|
try
|
|
{
|
|
_resources.Register(entity);
|
|
}
|
|
catch (Exception registrationError)
|
|
{
|
|
Exception? rollbackError = null;
|
|
try
|
|
{
|
|
_resources.Unregister(entity);
|
|
record.ResourcesRegistered = false;
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
rollbackError = error;
|
|
}
|
|
|
|
if (rollbackError is null)
|
|
{
|
|
_projections.RemoveActive(record);
|
|
record.ProjectionKey = null;
|
|
record.WorldEntity = null;
|
|
record = null;
|
|
throw;
|
|
}
|
|
|
|
_entityObjects.RetireAfterProjectionAcquisitionFailure(
|
|
expectedCanonical);
|
|
if (_projections.RemoveActive(record))
|
|
{
|
|
RetainTeardownRecord(record);
|
|
}
|
|
throw new AggregateException(
|
|
"Live entity resource registration and rollback both failed; " +
|
|
"the partial owner was retained for teardown retry.",
|
|
registrationError,
|
|
rollbackError);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_isRegisteringResources = false;
|
|
}
|
|
}
|
|
|
|
if (!_directory.IsCurrent(expectedCanonical)
|
|
|| !_projections.TryGet(expectedCanonical, out LiveEntityRecord currentRecord)
|
|
|| !ReferenceEquals(currentRecord, record))
|
|
{
|
|
record = null;
|
|
return null;
|
|
}
|
|
|
|
record.ProjectionKind = projectionKind;
|
|
if (projectionKind is LiveEntityProjectionKind.World)
|
|
{
|
|
// Attached projections inherit the complete ancestor visibility
|
|
// chain. Once the same logical entity becomes a top-level world
|
|
// projection it has no render ancestor, so retained inherited
|
|
// suppression must be cleared at this ownership transition.
|
|
record.WorldEntity!.IsAncestorDrawVisible = true;
|
|
}
|
|
RefreshPresentation(record);
|
|
WorldEntity materialized = record.WorldEntity!;
|
|
if (residence is LiveEntityMaterializationResidence
|
|
.AwaitRuntimePlacement)
|
|
{
|
|
// Resource construction is a logical App-side ownership edge, not
|
|
// world residence. Runtime's immutable Place receipt is the only
|
|
// path which may install the accepted frame and spatial bucket.
|
|
record.IsSpatiallyProjected = false;
|
|
record.IsSpatiallyVisible = false;
|
|
RefreshSpatialPresentationIndexes(record);
|
|
RefreshPresentation(record);
|
|
return materialized;
|
|
}
|
|
if (!RebucketLiveEntity(serverGuid, fullCellId)
|
|
|| !_projections.TryGet(expectedCanonical, out LiveEntityRecord? current)
|
|
|| !ReferenceEquals(current, record)
|
|
|| !ReferenceEquals(current.WorldEntity, materialized))
|
|
{
|
|
// Visibility observers may replace this GUID while rebucketing.
|
|
// A failed teardown can retain the displaced entity as a retry
|
|
// tombstone; it is not the result of this materialization.
|
|
record = null;
|
|
return null;
|
|
}
|
|
return materialized;
|
|
}
|
|
|
|
/// <summary>Changes spatial buckets only. No logical resource callbacks run.</summary>
|
|
public bool RebucketLiveEntity(uint serverGuid, uint spatialCellOrLandblockId)
|
|
{
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
|| record.WorldEntity is not { } entity)
|
|
return false;
|
|
if (record.MaterializationResidence is
|
|
LiveEntityMaterializationResidence.AwaitRuntimePlacement
|
|
&& HasActiveInitialCreateResidence(record.Canonical))
|
|
{
|
|
// C3c: while the initial-create residence is ACTIVE, Runtime's
|
|
// SetPosition owner is the sole canonical position/cell/
|
|
// object-clock authority and even the graphical bucket stays
|
|
// suppressed: the conductor's completion receipt (which reaches
|
|
// presentation through
|
|
// TryApplyInitialCreateCompletionPresentation, not this API) is
|
|
// the entity's first world-visible moment. Without this gate a
|
|
// re-entrant caller (e.g. a resource-registration observer)
|
|
// could install a bucket for a suppressed record before its
|
|
// placement ever committed. A STALE residence is lazily retired
|
|
// by this same query, after which legacy moves flow.
|
|
//
|
|
// C3c-R1 review R2: the gate is the EXACT-token residence
|
|
// activity view, never the sticky MaterializationResidence enum
|
|
// alone. Post-residence (the lease completed and was consumed)
|
|
// this method falls through to the FULL legacy branch below:
|
|
// the unflipped update routes (network position/state, remote
|
|
// and local teleports, streaming reprojection, hydration
|
|
// recovery) are the position authority again, and retail's
|
|
// prepare_to_enter_world (0x00511FA0) clock rebase must run on
|
|
// every root-workset membership edge — the earlier
|
|
// presentation-only shortcut skipped CommitRebucket and that
|
|
// clock edge for the entity's whole post-residence lifetime.
|
|
return false;
|
|
}
|
|
|
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
|
bool wasProjected = record.IsSpatiallyProjected;
|
|
bool wasVisible = record.IsSpatiallyVisible;
|
|
bool wasOrdinaryRoot = _physics.IsSpatialRoot(record.Canonical);
|
|
ulong projectionOperation = ++record.ProjectionMutationVersion;
|
|
// GpuWorldState reports an intermediate false/true pair while moving
|
|
// between two loaded buckets. Suppress those implementation details
|
|
// and publish only the final logical visibility edge.
|
|
record.IsSpatiallyProjected = true;
|
|
Exception? spatialNotificationFailure = null;
|
|
uint priorRebucketingGuid = _rebucketingGuid;
|
|
_rebucketingGuid = serverGuid;
|
|
try
|
|
{
|
|
try
|
|
{
|
|
_spatial.RebucketLiveEntity(
|
|
key,
|
|
entity,
|
|
spatialCellOrLandblockId);
|
|
}
|
|
catch (AggregateException error)
|
|
{
|
|
// GpuWorldState has already committed the bucket move and
|
|
// drained every visibility observer before reporting their
|
|
// failures. Finish this runtime transaction before surfacing
|
|
// the notification error to the caller.
|
|
spatialNotificationFailure = error;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_rebucketingGuid = priorRebucketingGuid;
|
|
}
|
|
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
|
|
{
|
|
ThrowAfterCommittedProjectionChange(
|
|
serverGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure: null);
|
|
return false;
|
|
}
|
|
bool visible = _spatial.IsLiveEntityProjectionResident(key);
|
|
record.IsSpatiallyVisible = visible;
|
|
RefreshPresentation(record);
|
|
uint committedFullCell =
|
|
(spatialCellOrLandblockId & 0xFFFFu) != 0xFFFFu
|
|
? spatialCellOrLandblockId
|
|
: record.FullCellId;
|
|
uint committedLandblock = spatialCellOrLandblockId == 0
|
|
? 0u
|
|
: (spatialCellOrLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
|
if (!_entityObjects.CommitRebucket(
|
|
record.Canonical,
|
|
committedFullCell,
|
|
committedLandblock))
|
|
{
|
|
ThrowAfterCommittedProjectionChange(
|
|
serverGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure: null);
|
|
return false;
|
|
}
|
|
bool isOrdinaryRoot = record.ProjectionKind is LiveEntityProjectionKind.World
|
|
&& record.IsSpatiallyProjected
|
|
&& record.IsSpatiallyVisible
|
|
&& record.FullCellId != 0;
|
|
// prepare_to_enter_world (0x00511FA0) writes update_time = cur_time.
|
|
// Only a root-workset membership edge rebases the clock. Ordinary
|
|
// loaded-to-loaded movement preserves it; parented/attached objects
|
|
// take retail update_object's parent early-out and remain suspended.
|
|
if (!wasProjected && !isOrdinaryRoot)
|
|
{
|
|
record.SuspendObjectClock();
|
|
SynchronizePhysicsBodyActiveState(record);
|
|
}
|
|
else if (wasOrdinaryRoot != isOrdinaryRoot)
|
|
{
|
|
if (isOrdinaryRoot)
|
|
{
|
|
// InitObjectBegin and every leave-visibility edge are inactive
|
|
// before retail prepare_to_enter_world. Preserve that
|
|
// prerequisite for a first materialization as well.
|
|
if (!wasProjected)
|
|
record.ObjectClock.Deactivate();
|
|
record.ResetObjectClockForEnterWorld(
|
|
(record.FinalPhysicsState & PhysicsStateFlags.Static) != 0);
|
|
SynchronizePhysicsBodyActiveState(record);
|
|
}
|
|
else
|
|
{
|
|
record.SuspendObjectClock();
|
|
SynchronizePhysicsBodyActiveState(record);
|
|
}
|
|
}
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
Exception? runtimeNotificationFailure = null;
|
|
if (!wasProjected || wasVisible != visible)
|
|
{
|
|
try
|
|
{
|
|
PublishProjectionVisibilityChanged(record, visible);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
runtimeNotificationFailure = error;
|
|
}
|
|
}
|
|
// Final observers are intentionally re-entrant. A callback can delete
|
|
// and recreate this GUID or start a newer rebucket on the same record;
|
|
// the outer caller must not continue with captures from that displaced
|
|
// projection operation.
|
|
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
|
|
{
|
|
ThrowAfterCommittedProjectionChange(
|
|
serverGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure);
|
|
return false;
|
|
}
|
|
ThrowAfterCommittedProjectionChange(
|
|
serverGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// C3c: the graphical-bucket-only projection of a conductor-owned
|
|
/// initial placement — called ONLY from
|
|
/// <see cref="TryApplyInitialCreateCompletionPresentation"/> (the
|
|
/// completion receipt at the initial-create residence boundary), never
|
|
/// from the public <see cref="RebucketLiveEntity"/> (C3c-R1 review R2:
|
|
/// post-residence moves take the full legacy branch there).
|
|
/// Deliberately never calls <c>CommitRebucket</c>,
|
|
/// <c>SuspendObjectClock</c>, or <c>ResetObjectClockForEnterWorld</c> —
|
|
/// Runtime's SetPosition commit already owns all of those for the
|
|
/// residence-driven placement this receipt projects. May place into a
|
|
/// pending (not-yet-loaded) bucket exactly like the legacy Create path
|
|
/// did; the pending drain publishes visibility when the landblock loads.
|
|
/// </summary>
|
|
private bool RebucketLiveEntityPresentationOnly(
|
|
uint serverGuid,
|
|
LiveEntityRecord record,
|
|
WorldEntity entity,
|
|
uint spatialCellOrLandblockId)
|
|
{
|
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
|
bool wasProjected = record.IsSpatiallyProjected;
|
|
bool wasVisible = record.IsSpatiallyVisible;
|
|
ulong projectionOperation = ++record.ProjectionMutationVersion;
|
|
record.IsSpatiallyProjected = true;
|
|
Exception? spatialNotificationFailure = null;
|
|
uint priorRebucketingGuid = _rebucketingGuid;
|
|
_rebucketingGuid = serverGuid;
|
|
BeginPresentationOnlySpatialMutation(key);
|
|
try
|
|
{
|
|
try
|
|
{
|
|
_spatial.RebucketLiveEntity(
|
|
key,
|
|
entity,
|
|
spatialCellOrLandblockId);
|
|
}
|
|
catch (AggregateException error)
|
|
{
|
|
spatialNotificationFailure = error;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
EndPresentationOnlySpatialMutation(key);
|
|
_rebucketingGuid = priorRebucketingGuid;
|
|
}
|
|
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
|
|
{
|
|
ThrowAfterCommittedProjectionChange(
|
|
serverGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure: null);
|
|
return false;
|
|
}
|
|
bool visible = _spatial.IsLiveEntityProjectionResident(key);
|
|
record.IsSpatiallyVisible = visible;
|
|
RefreshSpatialPresentationIndexes(record);
|
|
RefreshPresentation(record);
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
Exception? runtimeNotificationFailure = null;
|
|
if (!wasProjected || wasVisible != visible)
|
|
{
|
|
try
|
|
{
|
|
PublishProjectionVisibilityChanged(record, visible);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
runtimeNotificationFailure = error;
|
|
}
|
|
}
|
|
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
|
|
{
|
|
ThrowAfterCommittedProjectionChange(
|
|
serverGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure);
|
|
return false;
|
|
}
|
|
ThrowAfterCommittedProjectionChange(
|
|
serverGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// C3c: applies one initial-Create ExecutorCompleted receipt's
|
|
/// presentation — the graphical binding point for a residence-driven
|
|
/// initial placement. Runtime committed position, cell, body, clocks,
|
|
/// and worksets during the conductor's drain; this installs the
|
|
/// committed frame on the sidecar and moves its graphical bucket
|
|
/// (pending buckets allowed — the legacy Create path's own semantics).
|
|
/// Superseded facts (a later legacy-path move already advanced the
|
|
/// record past the receipt) are treated as already-projected: the
|
|
/// receipt is stale for presentation and must not snap the entity back.
|
|
/// </summary>
|
|
internal bool TryApplyInitialCreateCompletionPresentation(
|
|
in RuntimePlacementProjectionSnapshot projection)
|
|
{
|
|
RuntimePlacementProjectionToken token = projection.Token;
|
|
if (!token.IsValid
|
|
|| token.SessionLifetimeVersion != _directory.SessionLifetimeVersion
|
|
|| !_projections.TryGet(token.Entity, out LiveEntityRecord? record)
|
|
|| !_directory.IsCurrent(record.Canonical)
|
|
|| record.Canonical.Key != token.Entity
|
|
|| record.WorldEntity is not { } entity)
|
|
{
|
|
// No sidecar (a deferred-child replay materializes later and
|
|
// self-projects from canonical state) or a displaced identity —
|
|
// acknowledge-only.
|
|
return true;
|
|
}
|
|
if (record.FullCellId != token.ExactCellId
|
|
|| record.Canonical.PlacementCommitVersion
|
|
!= token.PlacementCommitVersion)
|
|
{
|
|
// A newer move superseded this receipt's facts after the drain.
|
|
return true;
|
|
}
|
|
|
|
entity.SetPosition(projection.WorldPosition);
|
|
entity.Rotation = projection.Orientation;
|
|
entity.ParentCellId = token.ExactCellId;
|
|
entity.EffectCellId = token.ExactCellId;
|
|
return RebucketLiveEntityPresentationOnly(
|
|
record.ServerGuid,
|
|
record,
|
|
entity,
|
|
token.ExactCellId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C3c: true while the exact incarnation behind <paramref name="key"/>
|
|
/// holds an initial-create residence lease — the discriminator the
|
|
/// placement sink uses to leave conductor-owned Place/Withdraw receipts
|
|
/// at the FIFO head for the drive controller to consume.
|
|
/// </summary>
|
|
internal bool HasActiveInitialCreateResidence(RuntimeEntityKey key) =>
|
|
_directory.TryGetByLocalId(
|
|
key.LocalEntityId,
|
|
out RuntimeEntityRecord canonical)
|
|
&& _directory.IsCurrent(canonical)
|
|
&& canonical.Key == key
|
|
&& _entityObjects.TryGetInitialCreateResidence(canonical, out _);
|
|
|
|
/// <summary>
|
|
/// C3c: true when the exact incarnation behind <paramref name="canonical"/>
|
|
/// holds an initial-create residence lease. Used by materialization to
|
|
/// decide whether presentation must await the conductor's completion
|
|
/// receipt or may self-project from already-committed canonical state.
|
|
/// </summary>
|
|
internal bool HasActiveInitialCreateResidence(
|
|
RuntimeEntityRecord canonical) =>
|
|
_entityObjects.TryGetInitialCreateResidence(canonical, out _);
|
|
|
|
/// <summary>
|
|
/// C3c-R1 review F1: the ONLY sanctioned mutation of the otherwise
|
|
/// sticky <see cref="LiveEntityRecord.MaterializationResidence"/> — a
|
|
/// world-created (residence-managed) entity converting to an attached
|
|
/// projection at a same-incarnation kind transition (the equipped-child
|
|
/// world→attached path). Attached children have no Runtime placement,
|
|
/// so the sticky-residence rule expects them to carry
|
|
/// <see cref="LiveEntityMaterializationResidence.LegacyImmediate"/>.
|
|
/// Owned here so the invariant is asserted at the owner: converting
|
|
/// while the initial-create residence is still ACTIVE would let an
|
|
/// attached materialization race the conductor's pending placement.
|
|
/// </summary>
|
|
internal void ConvertMaterializationResidenceToLegacyImmediate(
|
|
LiveEntityRecord record)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
if (record.MaterializationResidence is not
|
|
LiveEntityMaterializationResidence.AwaitRuntimePlacement)
|
|
{
|
|
return;
|
|
}
|
|
if (HasActiveInitialCreateResidence(record.Canonical))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Live entity 0x{record.ServerGuid:X8}/"
|
|
+ $"{record.Canonical.Incarnation} cannot convert to "
|
|
+ "LegacyImmediate residence while its initial-create "
|
|
+ "residence lease is still active.");
|
|
}
|
|
record.MaterializationResidence =
|
|
LiveEntityMaterializationResidence.LegacyImmediate;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies one canonical Runtime placement receipt to the graphical
|
|
/// sidecar only. Runtime has already committed identity, position,
|
|
/// collision residence, object-clock state, and simulation worksets.
|
|
/// </summary>
|
|
internal bool TryApplyRuntimePlacementProjection(
|
|
in RuntimePlacementProjectionSnapshot projection)
|
|
{
|
|
if (projection.Kind is RuntimePlacementProjectionKind.Discard)
|
|
{
|
|
// Discard cancels only an unacknowledged observation. If its Place
|
|
// was already projected, retail keeps that last committed frame
|
|
// visible until a later canonical Place or Withdraw supersedes it.
|
|
return true;
|
|
}
|
|
|
|
if (projection.Kind is RuntimePlacementProjectionKind.ExecutorCompleted)
|
|
{
|
|
// F1: acknowledge-and-ignore, same as Discard. Must NOT fall
|
|
// through to the record-lookup/spatial-load gates below - those
|
|
// legitimately reject for reasons unrelated to this receipt (no
|
|
// sidecar yet, destination backend not loaded), and a false
|
|
// return here wedges the whole ordered placement stream at the
|
|
// FIFO head (RuntimePlacementProjectionSubscription's contract).
|
|
// Provably inert today: PublishExecutorCompletion has zero
|
|
// production callers.
|
|
return true;
|
|
}
|
|
|
|
RuntimePlacementProjectionToken token = projection.Token;
|
|
if (!TryGetRuntimePlacementProjectionRecord(
|
|
token,
|
|
requirePlacementVersions:
|
|
projection.Kind is RuntimePlacementProjectionKind.Place,
|
|
out LiveEntityRecord? record)
|
|
|| record.WorldEntity is not { } entity)
|
|
{
|
|
return false;
|
|
}
|
|
if (projection.Kind is RuntimePlacementProjectionKind.Place
|
|
&& !_spatial.IsLoaded(
|
|
(token.ExactCellId & 0xFFFF0000u) | 0xFFFFu))
|
|
{
|
|
// A canonical SetPosition receipt is not permission to create a
|
|
// pending graphical bucket. Keep the FIFO head unacknowledged
|
|
// until the destination backend exists, otherwise GpuWorldState's
|
|
// later pending-drain edge escapes this exact receipt transaction.
|
|
return false;
|
|
}
|
|
|
|
return projection.Kind switch
|
|
{
|
|
RuntimePlacementProjectionKind.Place =>
|
|
TryApplyRuntimePlacementPlace(in projection, record, entity),
|
|
RuntimePlacementProjectionKind.Withdraw =>
|
|
TryApplyRuntimePlacementWithdrawal(token, record, entity),
|
|
_ => false,
|
|
};
|
|
}
|
|
|
|
private bool TryApplyRuntimePlacementPlace(
|
|
in RuntimePlacementProjectionSnapshot projection,
|
|
LiveEntityRecord record,
|
|
WorldEntity entity)
|
|
{
|
|
RuntimePlacementProjectionToken token = projection.Token;
|
|
RuntimeEntityKey key = token.Entity;
|
|
ulong projectionOperation = ++record.ProjectionMutationVersion;
|
|
|
|
entity.SetPosition(projection.WorldPosition);
|
|
entity.Rotation = projection.Orientation;
|
|
entity.ParentCellId = token.ExactCellId;
|
|
entity.EffectCellId = token.ExactCellId;
|
|
record.IsSpatiallyProjected = true;
|
|
|
|
Exception? spatialNotificationFailure = null;
|
|
uint priorRebucketingGuid = _rebucketingGuid;
|
|
_rebucketingGuid = record.ServerGuid;
|
|
BeginPresentationOnlySpatialMutation(key);
|
|
try
|
|
{
|
|
try
|
|
{
|
|
_spatial.RebucketLiveEntity(key, entity, token.ExactCellId);
|
|
}
|
|
catch (AggregateException error)
|
|
{
|
|
spatialNotificationFailure = error;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
EndPresentationOnlySpatialMutation(key);
|
|
_rebucketingGuid = priorRebucketingGuid;
|
|
}
|
|
|
|
if (!IsCurrentProjectionOperation(
|
|
record.ServerGuid,
|
|
record,
|
|
projectionOperation)
|
|
|| !TryGetRuntimePlacementProjectionRecord(
|
|
token,
|
|
requirePlacementVersions: true,
|
|
out LiveEntityRecord? current)
|
|
|| !ReferenceEquals(current, record)
|
|
|| !ReferenceEquals(current.WorldEntity, entity))
|
|
{
|
|
ThrowAfterCommittedProjectionChange(
|
|
record.ServerGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure: null);
|
|
return false;
|
|
}
|
|
|
|
bool visible = _spatial.IsLiveEntityProjectionResident(key);
|
|
record.IsSpatiallyVisible = visible;
|
|
RefreshSpatialPresentationIndexes(record);
|
|
RefreshPresentation(record);
|
|
|
|
if (!IsCurrentProjectionOperation(
|
|
record.ServerGuid,
|
|
record,
|
|
projectionOperation))
|
|
{
|
|
ThrowAfterCommittedProjectionChange(
|
|
record.ServerGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure: null);
|
|
return false;
|
|
}
|
|
|
|
ThrowAfterCommittedProjectionChange(
|
|
record.ServerGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure: null);
|
|
return true;
|
|
}
|
|
|
|
private bool TryApplyRuntimePlacementWithdrawal(
|
|
in RuntimePlacementProjectionToken token,
|
|
LiveEntityRecord record,
|
|
WorldEntity entity)
|
|
{
|
|
RuntimeEntityKey key = token.Entity;
|
|
ulong projectionOperation = ++record.ProjectionMutationVersion;
|
|
record.IsSpatiallyProjected = false;
|
|
|
|
Exception? spatialNotificationFailure = null;
|
|
uint priorRebucketingGuid = _rebucketingGuid;
|
|
_rebucketingGuid = record.ServerGuid;
|
|
BeginPresentationOnlySpatialMutation(key);
|
|
try
|
|
{
|
|
try
|
|
{
|
|
_spatial.RemoveLiveEntityProjection(entity);
|
|
}
|
|
catch (AggregateException error)
|
|
{
|
|
spatialNotificationFailure = error;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
EndPresentationOnlySpatialMutation(key);
|
|
_rebucketingGuid = priorRebucketingGuid;
|
|
}
|
|
|
|
if (!IsCurrentProjectionOperation(
|
|
record.ServerGuid,
|
|
record,
|
|
projectionOperation)
|
|
|| !TryGetRuntimePlacementProjectionRecord(
|
|
token,
|
|
requirePlacementVersions: false,
|
|
out LiveEntityRecord? current)
|
|
|| !ReferenceEquals(current, record)
|
|
|| !ReferenceEquals(current.WorldEntity, entity))
|
|
{
|
|
ThrowAfterCommittedProjectionChange(
|
|
record.ServerGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure: null);
|
|
return false;
|
|
}
|
|
|
|
record.IsSpatiallyVisible = false;
|
|
RefreshSpatialPresentationIndexes(record);
|
|
RefreshPresentation(record);
|
|
|
|
if (!IsCurrentProjectionOperation(
|
|
record.ServerGuid,
|
|
record,
|
|
projectionOperation))
|
|
{
|
|
ThrowAfterCommittedProjectionChange(
|
|
record.ServerGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure: null);
|
|
return false;
|
|
}
|
|
|
|
ThrowAfterCommittedProjectionChange(
|
|
record.ServerGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure: null);
|
|
return true;
|
|
}
|
|
|
|
private bool TryGetRuntimePlacementProjectionRecord(
|
|
in RuntimePlacementProjectionToken token,
|
|
bool requirePlacementVersions,
|
|
out LiveEntityRecord record)
|
|
{
|
|
if (!token.IsValid
|
|
|| token.SessionLifetimeVersion != _directory.SessionLifetimeVersion
|
|
|| !_projections.TryGet(token.Entity, out record!)
|
|
|| !_directory.IsCurrent(record.Canonical)
|
|
|| record.Canonical.Key != token.Entity
|
|
|| RequireProjectionKey(record) != token.Entity
|
|
|| !IsValidPortalPlacementAuthority(token))
|
|
{
|
|
record = null!;
|
|
return false;
|
|
}
|
|
|
|
if (requirePlacementVersions
|
|
&& (record.Canonical.PositionAuthorityVersion
|
|
!= token.PositionAuthorityVersion
|
|
|| record.Canonical.SpatialAuthorityVersion
|
|
!= token.SpatialAuthorityVersion
|
|
|| record.Canonical.PlacementCommitVersion
|
|
!= token.PlacementCommitVersion
|
|
|| record.Canonical.FullCellId != token.ExactCellId))
|
|
{
|
|
record = null!;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool IsValidPortalPlacementAuthority(
|
|
in RuntimePlacementProjectionToken token)
|
|
{
|
|
RuntimePortalPlacementAuthority portal = token.Portal;
|
|
if (!portal.Present)
|
|
return portal.IsEmpty;
|
|
|
|
return portal.IsValid
|
|
&& portal.Projection.DestinationCell == token.ExactCellId;
|
|
}
|
|
|
|
private void BeginPresentationOnlySpatialMutation(RuntimeEntityKey key)
|
|
{
|
|
_presentationOnlySpatialMutationDepth.TryGetValue(key, out int depth);
|
|
_presentationOnlySpatialMutationDepth[key] = checked(depth + 1);
|
|
}
|
|
|
|
private void EndPresentationOnlySpatialMutation(RuntimeEntityKey key)
|
|
{
|
|
if (!_presentationOnlySpatialMutationDepth.TryGetValue(
|
|
key,
|
|
out int depth)
|
|
|| depth <= 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Presentation-only spatial mutation depth was not balanced.");
|
|
}
|
|
|
|
if (depth == 1)
|
|
_presentationOnlySpatialMutationDepth.Remove(key);
|
|
else
|
|
_presentationOnlySpatialMutationDepth[key] = depth - 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes only the render-bucket reference. The logical record and every
|
|
/// create-time resource remain alive for later projection/rebucketing.
|
|
/// </summary>
|
|
public bool WithdrawLiveEntityProjection(uint serverGuid)
|
|
{
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
|| record.WorldEntity is null)
|
|
return false;
|
|
|
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
|
bool wasOrdinaryRoot = _physics.IsSpatialRoot(record.Canonical);
|
|
ulong objectClockEpoch = record.ObjectClockEpoch;
|
|
ulong projectionOperation = ++record.ProjectionMutationVersion;
|
|
Exception? spatialNotificationFailure = null;
|
|
try
|
|
{
|
|
_spatial.RemoveLiveEntityProjection(record.WorldEntity);
|
|
}
|
|
catch (AggregateException error)
|
|
{
|
|
// The projection is already absent and GpuWorldState has already
|
|
// drained its observer queue. Complete canonical withdrawal below
|
|
// before reporting the notification failure.
|
|
spatialNotificationFailure = error;
|
|
}
|
|
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
|
|
{
|
|
ThrowAfterCommittedProjectionChange(
|
|
serverGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure: null);
|
|
return false;
|
|
}
|
|
// Usually GpuWorldState delivers the false edge synchronously above.
|
|
// During a reentrant visibility callback it queues that edge until the
|
|
// outer notification completes, so publish the logical withdrawal now;
|
|
// OnSpatialVisibilityChanged rejects the later duplicate against this
|
|
// committed record state.
|
|
bool spatialEdgeWasDeferred = record.IsSpatiallyVisible;
|
|
_projections.SetVisible(record, false);
|
|
record.IsSpatiallyProjected = false;
|
|
record.IsSpatiallyVisible = false;
|
|
// GpuWorldState normally publishes the leave edge synchronously and
|
|
// OnSpatialVisibilityChanged suspends this exact clock there. During a
|
|
// reentrant notification the edge is deferred, so commit the missing
|
|
// suspension here. One root-workset leave is one epoch edge -- never
|
|
// advance it once in the observer and again in this owner.
|
|
if (wasOrdinaryRoot && record.ObjectClockEpoch == objectClockEpoch)
|
|
record.SuspendObjectClock();
|
|
SynchronizePhysicsBodyActiveState(record);
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
Exception? runtimeNotificationFailure = null;
|
|
if (spatialEdgeWasDeferred)
|
|
{
|
|
try
|
|
{
|
|
PublishProjectionVisibilityChanged(record, false);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
runtimeNotificationFailure = error;
|
|
}
|
|
}
|
|
// The manually published deferred edge is an arbitrary callback
|
|
// boundary just like Rebucket's final visibility edge. It may
|
|
// reproject this record or replace the GUID; the displaced outer
|
|
// withdrawal must not report itself as the current operation.
|
|
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
|
|
{
|
|
ThrowAfterCommittedProjectionChange(
|
|
serverGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure);
|
|
return false;
|
|
}
|
|
ThrowAfterCommittedProjectionChange(
|
|
serverGuid,
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exact-incarnation withdrawal used after App leave-world callbacks. A
|
|
/// callback may accept a newer INSTANCE_TS for the same GUID; that newer
|
|
/// projection must never be withdrawn by the displaced operation.
|
|
/// </summary>
|
|
public bool WithdrawLiveEntityProjection(LiveEntityRecord expectedRecord)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
return WithdrawLiveEntityProjection(
|
|
expectedRecord,
|
|
expectedRecord.PositionAuthorityVersion,
|
|
expectedRecord.ProjectionMutationVersion);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exact-operation withdrawal used across arbitrary App callbacks. Both
|
|
/// the accepted Position authority and projection mutation token are
|
|
/// pinned so a same-incarnation re-entry cannot be removed by an older
|
|
/// parent/pickup/leave-world operation.
|
|
/// </summary>
|
|
internal bool WithdrawLiveEntityProjection(
|
|
LiveEntityRecord expectedRecord,
|
|
ulong expectedPositionAuthorityVersion,
|
|
ulong expectedProjectionMutationVersion)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
return IsCurrentRecord(expectedRecord)
|
|
&& expectedRecord.PositionAuthorityVersion == expectedPositionAuthorityVersion
|
|
&& expectedRecord.ProjectionMutationVersion == expectedProjectionMutationVersion
|
|
&& WithdrawLiveEntityProjection(expectedRecord.ServerGuid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Withdraws a still-live projection whose authoritative rollback frame
|
|
/// is outside every world cell. Logical owners remain registered.
|
|
/// </summary>
|
|
internal bool WithdrawLiveEntityProjectionToCellless(uint serverGuid)
|
|
{
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
|| record.WorldEntity is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!_entityObjects.CommitWithdrawal(
|
|
record.Canonical,
|
|
AcknowledgeCelllessCanonicalCommit))
|
|
{
|
|
return false;
|
|
}
|
|
record.WorldEntity.ParentCellId = 0u;
|
|
return WithdrawLiveEntityProjection(serverGuid);
|
|
}
|
|
|
|
public bool UnregisterLiveEntity(
|
|
DeleteObject.Parsed delete,
|
|
bool isLocalPlayer,
|
|
bool removeRetainedObject = false,
|
|
Action? beforeTeardown = null)
|
|
{
|
|
if (_isRegisteringResources)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"A live entity cannot unregister from inside atomic resource registration.");
|
|
}
|
|
var teardownKey = (delete.Guid, delete.InstanceSequence);
|
|
LiveEntityRecord? record = null;
|
|
RuntimeEntityRecord? canonicalOnly = null;
|
|
if (_directory.TryGetTeardown(
|
|
teardownKey.Guid,
|
|
teardownKey.InstanceSequence,
|
|
out RuntimeEntityRecord retainedCanonical))
|
|
{
|
|
_projections.TryGetTeardown(retainedCanonical, out record);
|
|
}
|
|
bool retryingAcceptedDelete = record is not null
|
|
&& (record.DeleteAcceptedForTeardown
|
|
|| _isClearing
|
|
|| _sessionClearPendingFinalization);
|
|
RuntimeEntityDeleteAcceptance? acceptedDelete = null;
|
|
if (!retryingAcceptedDelete)
|
|
{
|
|
if (!_entityObjects.TryAcceptDelete(
|
|
delete,
|
|
isLocalPlayer,
|
|
removeRetainedObject,
|
|
out acceptedDelete))
|
|
return false;
|
|
|
|
// End active gameplay identity before arbitrary callbacks so a
|
|
// newer generation can be accepted re-entrantly, but retain the
|
|
// exact incarnation as a teardown tombstone until unregister has
|
|
// succeeded. A throwing resource callback can then be retried by
|
|
// the same accepted Delete instead of orphaning its owner.
|
|
LiveEntityRecord? retainedRegistrationFailure = record;
|
|
if (acceptedDelete.RetiredCanonical is { } activeCanonical)
|
|
{
|
|
if (_projections.TryGet(
|
|
activeCanonical,
|
|
out LiveEntityRecord? activeRecord))
|
|
{
|
|
if (!_projections.RemoveActive(activeRecord))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Exact App projection for 0x{delete.Guid:X8}/{delete.InstanceSequence} could not be retired.");
|
|
}
|
|
record = activeRecord;
|
|
RetainTeardownRecord(record);
|
|
}
|
|
else
|
|
{
|
|
canonicalOnly = activeCanonical;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
record = retainedRegistrationFailure;
|
|
}
|
|
if (record is not null)
|
|
record.DeleteAcceptedForTeardown = true;
|
|
}
|
|
|
|
List<Exception>? failures = null;
|
|
_logicalTeardownDepth++;
|
|
try
|
|
{
|
|
if (!retryingAcceptedDelete)
|
|
{
|
|
try
|
|
{
|
|
_entityObjects.CompleteAcceptedDelete(acceptedDelete!);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= new List<Exception>()).Add(error);
|
|
}
|
|
|
|
try
|
|
{
|
|
beforeTeardown?.Invoke();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= new List<Exception>()).Add(error);
|
|
}
|
|
}
|
|
|
|
if (record is not null)
|
|
{
|
|
try
|
|
{
|
|
TearDownRecord(record);
|
|
ReleaseTeardownRecord(record);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= new List<Exception>()).Add(error);
|
|
}
|
|
}
|
|
else if (canonicalOnly is not null)
|
|
{
|
|
TearDownCanonicalOnly(canonicalOnly);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_logicalTeardownDepth--;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Persistence is GUID-scoped. End it only when the accepted
|
|
// delete left no replacement incarnation or unfinished teardown
|
|
// behind.
|
|
if (!_directory.TryGetActive(delete.Guid, out _)
|
|
&& !HasPendingTeardown(delete.Guid))
|
|
{
|
|
_spatial.ForgetLiveEntity(delete.Guid);
|
|
}
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= new List<Exception>()).Add(error);
|
|
}
|
|
|
|
if (failures is not null)
|
|
throw new AggregateException(
|
|
$"Live entity 0x{delete.Guid:X8} deletion cleanup failed.",
|
|
failures);
|
|
return true;
|
|
}
|
|
|
|
public bool TryGetRecord(uint serverGuid, out LiveEntityRecord record) =>
|
|
_projections.TryGetCurrent(serverGuid, out record!);
|
|
|
|
public bool TryGetWorldEntity(uint serverGuid, out WorldEntity entity)
|
|
{
|
|
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
&& record.WorldEntity is { } found)
|
|
{
|
|
entity = found;
|
|
return true;
|
|
}
|
|
|
|
entity = null!;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a current top-level world projection even when its spatial
|
|
/// bucket is pending/nonresident. Unlike interaction eligibility, this
|
|
/// does not impose render visibility; unlike <see cref="TryGetWorldEntity"/>,
|
|
/// it rejects a retained leave-world owner whose pose is historical.
|
|
/// </summary>
|
|
public bool TryGetSpatiallyProjectedRecord(
|
|
uint serverGuid,
|
|
out LiveEntityRecord record)
|
|
{
|
|
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord found)
|
|
&& found.WorldEntity is not null
|
|
&& found.ProjectionKind is LiveEntityProjectionKind.World
|
|
&& found.IsSpatiallyProjected)
|
|
{
|
|
record = found;
|
|
return true;
|
|
}
|
|
|
|
record = null!;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a current equipped-child projection whose composed frame is
|
|
/// live. Retail installs an equipped item as a first-class
|
|
/// <c>CPhysicsObj</c> with its own object id and part array
|
|
/// (<c>CPhysicsObj::add_child @ 0x0050F870</c> via
|
|
/// <c>CSetup::GetHoldingLocation @ 0x005213F0</c>), and
|
|
/// <c>CPhysicsObj::UpdateChild @ 0x00512D50</c> recomposes
|
|
/// <c>Frame::combine(parent part frame, holding frame)</c> into that child's
|
|
/// own <c>m_position</c> every frame. An attached projection therefore has
|
|
/// real world presence even though it is deliberately absent from the
|
|
/// interaction/radar/auto-target visible set.
|
|
/// </summary>
|
|
public bool TryGetAttachedProjectedRecord(
|
|
uint serverGuid,
|
|
out LiveEntityRecord record)
|
|
{
|
|
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord found)
|
|
&& found.WorldEntity is not null
|
|
&& found.ProjectionKind is LiveEntityProjectionKind.Attached
|
|
&& found.IsSpatiallyProjected)
|
|
{
|
|
record = found;
|
|
return true;
|
|
}
|
|
|
|
record = null!;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves an object that currently participates in mouse picking.
|
|
/// Retail's only candidacy rule is "a drawn part whose owning physics
|
|
/// object has a nonzero id" (<c>CPhysicsPart::Draw @ 0x0050D7A0</c> guards
|
|
/// on <c>CPhysicsPart::get_physobj_id @ 0x0050D490</c>, and
|
|
/// <c>Render::GfxObjUnderSelectionRay @ 0x0054C740</c> accumulates the
|
|
/// hit under that id) — there is no parent redirection and no
|
|
/// wielded-specific gate, so a click on a wielded weapon returns the
|
|
/// weapon's own GUID. Picking therefore admits attached projections on top
|
|
/// of the ordinary top-level visible set. This is deliberately NOT the
|
|
/// interaction-eligible predicate: radar, auto-target, and
|
|
/// MoveTo/Sticky establishment must stay wielded-item free.
|
|
/// </summary>
|
|
public bool TryGetPickEligibleRecord(
|
|
uint serverGuid,
|
|
out LiveEntityRecord record)
|
|
=> TryGetInteractionEligibleRecord(serverGuid, out record)
|
|
|| TryGetAttachedProjectedRecord(serverGuid, out record);
|
|
|
|
/// <summary>
|
|
/// Pick eligibility bound to one logical incarnation. A stale published
|
|
/// frame must never retarget a replacement which reused the server GUID.
|
|
/// </summary>
|
|
public bool TryGetPickEligibleRecord(
|
|
uint serverGuid,
|
|
uint localEntityId,
|
|
out LiveEntityRecord record)
|
|
{
|
|
if (serverGuid != 0u
|
|
&& localEntityId != 0u
|
|
&& TryGetPickEligibleRecord(serverGuid, out LiveEntityRecord found)
|
|
&& found.WorldEntity!.Id == localEntityId)
|
|
{
|
|
record = found;
|
|
return true;
|
|
}
|
|
|
|
record = null!;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a top-level object that currently participates in
|
|
/// targeting, radar, and wire-driven MoveTo/Sticky establishment.
|
|
/// Pending, attached, and Hidden projections are intentionally excluded.
|
|
/// </summary>
|
|
public bool TryGetInteractionEligibleEntity(
|
|
uint serverGuid,
|
|
out WorldEntity entity)
|
|
{
|
|
if (_projections.TryGetVisibleCurrent(
|
|
serverGuid,
|
|
out LiveEntityRecord record)
|
|
&& record.WorldEntity is { } visible)
|
|
{
|
|
entity = visible;
|
|
return true;
|
|
}
|
|
|
|
entity = null!;
|
|
return false;
|
|
}
|
|
|
|
bool ILiveEntityRadarSource.TryGetMaterialized(
|
|
uint serverGuid,
|
|
out WorldEntity entity) =>
|
|
TryGetWorldEntity(serverGuid, out entity);
|
|
|
|
bool ILiveEntityRadarSource.TryGetVisible(
|
|
uint serverGuid,
|
|
out WorldEntity entity) =>
|
|
TryGetInteractionEligibleEntity(serverGuid, out entity);
|
|
|
|
void ILiveEntityRadarSource.CopyVisibleTo(
|
|
List<KeyValuePair<uint, WorldEntity>> destination)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(destination);
|
|
destination.Clear();
|
|
foreach (LiveEntityRecord record in _projections.VisibleRecords)
|
|
{
|
|
destination.Add(new KeyValuePair<uint, WorldEntity>(
|
|
record.ServerGuid,
|
|
record.WorldEntity!));
|
|
}
|
|
}
|
|
|
|
public bool TryGetInteractionEligibleRecord(
|
|
uint serverGuid,
|
|
out LiveEntityRecord record)
|
|
{
|
|
if (_projections.TryGetVisibleCurrent(
|
|
serverGuid,
|
|
out LiveEntityRecord found)
|
|
&& found.WorldEntity is not null)
|
|
{
|
|
record = found;
|
|
return true;
|
|
}
|
|
|
|
record = null!;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a render-published interaction candidate only while the same
|
|
/// logical WorldEntity incarnation still owns the server GUID. A stale
|
|
/// frame must never retarget a replacement which reused that GUID.
|
|
/// </summary>
|
|
public bool TryGetInteractionEligibleRecord(
|
|
uint serverGuid,
|
|
uint localEntityId,
|
|
out LiveEntityRecord record)
|
|
{
|
|
if (serverGuid != 0u
|
|
&& localEntityId != 0u
|
|
&& TryGetInteractionEligibleRecord(serverGuid, out LiveEntityRecord found)
|
|
&& found.WorldEntity!.Id == localEntityId)
|
|
{
|
|
record = found;
|
|
return true;
|
|
}
|
|
|
|
record = null!;
|
|
return false;
|
|
}
|
|
|
|
public bool ContainsWorldEntity(uint serverGuid) =>
|
|
_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
&& record.WorldEntity is not null;
|
|
|
|
public bool TryGetServerGuid(uint localEntityId, out uint serverGuid)
|
|
{
|
|
if (_directory.TryGetByLocalId(
|
|
localEntityId,
|
|
out RuntimeEntityRecord canonical))
|
|
{
|
|
serverGuid = canonical.ServerGuid;
|
|
return true;
|
|
}
|
|
|
|
serverGuid = 0u;
|
|
return false;
|
|
}
|
|
|
|
internal bool TryGetRecordByLocalEntityId(
|
|
uint localEntityId,
|
|
out LiveEntityRecord record) =>
|
|
_projections.TryGetByLocalId(localEntityId, out record);
|
|
|
|
internal bool TryGetRecord(
|
|
RuntimeEntityKey key,
|
|
out LiveEntityRecord record) =>
|
|
_projections.TryGet(key, out record);
|
|
|
|
public bool TryGetLocalEntityId(uint serverGuid, out uint localEntityId)
|
|
{
|
|
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
&& record.LocalEntityId is { } found)
|
|
{
|
|
localEntityId = found;
|
|
return true;
|
|
}
|
|
|
|
localEntityId = 0;
|
|
return false;
|
|
}
|
|
|
|
public void SetAnimationRuntime(uint serverGuid, ILiveEntityAnimationRuntime runtime)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(runtime);
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
|| record.WorldEntity is not { } entity)
|
|
throw new InvalidOperationException($"Cannot bind animation before live entity 0x{serverGuid:X8} is materialized.");
|
|
if (!ReferenceEquals(runtime.Entity, entity))
|
|
throw new InvalidOperationException("Animation runtime belongs to a different WorldEntity.");
|
|
|
|
_ = RequireProjectionKey(record);
|
|
record.AnimationRuntime = runtime;
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
}
|
|
|
|
public bool TryGetAnimationRuntime(
|
|
uint localEntityId,
|
|
out ILiveEntityAnimationRuntime runtime)
|
|
{
|
|
if (_projections.TryGetByLocalId(
|
|
localEntityId,
|
|
out LiveEntityRecord record)
|
|
&& record.AnimationRuntime is { } found)
|
|
{
|
|
runtime = found;
|
|
return true;
|
|
}
|
|
|
|
runtime = null!;
|
|
return false;
|
|
}
|
|
|
|
internal bool TryGetProjectionKey(
|
|
uint serverGuid,
|
|
out RuntimeEntityKey key)
|
|
{
|
|
if (_projections.TryGetCurrent(
|
|
serverGuid,
|
|
out LiveEntityRecord? record)
|
|
&& record.ProjectionKey is { } found)
|
|
{
|
|
key = found;
|
|
return true;
|
|
}
|
|
|
|
key = default;
|
|
return false;
|
|
}
|
|
|
|
public bool ClearAnimationRuntime(uint serverGuid)
|
|
{
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
|| record.AnimationRuntime is null)
|
|
return false;
|
|
record.AnimationRuntime = null;
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
return true;
|
|
}
|
|
|
|
internal bool ClearAnimationRuntime(LiveEntityRecord expectedRecord)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
if (expectedRecord.AnimationRuntime is null
|
|
|| expectedRecord.ProjectionKey is not { } key
|
|
|| !_projections.TryGet(key, out LiveEntityRecord? record)
|
|
|| !ReferenceEquals(record, expectedRecord))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
record.AnimationRuntime = null;
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Acquires the one retail <c>CPhysicsObj</c> body for this exact live
|
|
/// incarnation. Static animation may need the body before a MovementManager
|
|
/// or projectile component exists; later components must adopt this same
|
|
/// instance instead of replaying CreateObject vector initialization.
|
|
/// </summary>
|
|
internal PhysicsBody GetOrCreatePhysicsBody(
|
|
uint serverGuid,
|
|
Func<LiveEntityRecord, PhysicsBody> factory)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(factory);
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
|| record.WorldEntity is null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Cannot acquire physics body before live entity 0x{serverGuid:X8} is materialized.");
|
|
}
|
|
bool ProjectionIsCurrent() =>
|
|
_projections.TryGetCurrent(
|
|
serverGuid,
|
|
out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record)
|
|
&& current.WorldEntity is not null;
|
|
return _physics.GetOrCreatePhysicsBody(
|
|
record.Canonical,
|
|
_ => factory(record),
|
|
ProjectionIsCurrent);
|
|
}
|
|
|
|
internal void SetRemoteMotionRuntime(uint serverGuid, IRuntimeRemoteMotion runtime)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(runtime);
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record))
|
|
throw new InvalidOperationException($"Cannot bind remote motion before live entity 0x{serverGuid:X8} exists.");
|
|
ulong lifetimeVersion = CurrentLifetimeMutation(serverGuid);
|
|
_physics.SetRemoteMotion(
|
|
record.Canonical,
|
|
runtime,
|
|
() => CurrentLifetimeMutation(serverGuid) == lifetimeVersion
|
|
&& _projections.TryGetCurrent(
|
|
serverGuid,
|
|
out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record));
|
|
SynchronizePhysicsBodyActiveState(record);
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
}
|
|
|
|
public void InstallPhysicsHost(
|
|
LiveEntityRecord expectedRecord,
|
|
AcDream.Core.Physics.Motion.IPhysicsObjHost host)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
ArgumentNullException.ThrowIfNull(host);
|
|
uint serverGuid = expectedRecord.ServerGuid;
|
|
bool ProjectionIsCurrent() =>
|
|
_projections.TryGetCurrent(
|
|
serverGuid,
|
|
out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, expectedRecord);
|
|
_physics.InstallPhysicsHost(
|
|
expectedRecord.Canonical,
|
|
host,
|
|
ProjectionIsCurrent);
|
|
}
|
|
|
|
public bool TryGetPhysicsHost(
|
|
uint serverGuid,
|
|
out AcDream.Core.Physics.Motion.IPhysicsObjHost host)
|
|
{
|
|
if (_projections.TryGetCurrent(serverGuid, out _)
|
|
&& _physics.TryGetPhysicsHost(serverGuid, out var existing))
|
|
{
|
|
host = existing;
|
|
return true;
|
|
}
|
|
|
|
host = null!;
|
|
return false;
|
|
}
|
|
|
|
public bool TryGetRemoteMotionRuntime(
|
|
uint serverGuid,
|
|
out IRuntimeRemoteMotion runtime)
|
|
{
|
|
if (_projections.TryGetCurrent(
|
|
serverGuid,
|
|
out LiveEntityRecord? record)
|
|
&& record.RemoteMotionRuntime is { } found)
|
|
{
|
|
runtime = found;
|
|
return true;
|
|
}
|
|
|
|
runtime = null!;
|
|
return false;
|
|
}
|
|
|
|
internal bool ClearRemoteMotionRuntime(uint serverGuid)
|
|
{
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
|| record.RemoteMotionRuntime is null)
|
|
return false;
|
|
if (!_physics.ClearRemoteMotion(record.Canonical))
|
|
return false;
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
return true;
|
|
}
|
|
|
|
internal RemoteMotion GetOrCreateRemoteMotionRuntime(uint serverGuid)
|
|
{
|
|
if (!_projections.TryGetCurrent(
|
|
serverGuid,
|
|
out LiveEntityRecord? record))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Cannot acquire remote motion before live entity 0x{serverGuid:X8} exists.");
|
|
}
|
|
|
|
bool ProjectionIsCurrent() =>
|
|
_projections.TryGetCurrent(
|
|
serverGuid,
|
|
out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record);
|
|
return _physics.GetOrCreateRemoteMotion(
|
|
record.Canonical,
|
|
ProjectionIsCurrent);
|
|
}
|
|
|
|
internal IRuntimeProjectile BindProjectileRuntime(
|
|
uint serverGuid,
|
|
PhysicsBody body,
|
|
ProjectileCollisionSphere collisionSphere,
|
|
Func<bool>? externalOwnerValid = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(body);
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
|| record.WorldEntity is null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Cannot bind projectile physics before live entity 0x{serverGuid:X8} is materialized.");
|
|
}
|
|
if (record.PhysicsBodyAcquisitionInProgress && record.PhysicsBody is null)
|
|
throw new InvalidOperationException(
|
|
$"Live entity 0x{serverGuid:X8} cannot bind projectile physics during physics-body acquisition.");
|
|
if (record.PhysicsBody is { } canonicalBody
|
|
&& !ReferenceEquals(canonicalBody, body))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Live entity 0x{serverGuid:X8} cannot replace its canonical physics body within one incarnation.");
|
|
}
|
|
|
|
IRuntimeProjectile runtime = _physics.BindProjectile(
|
|
record.Canonical,
|
|
body,
|
|
collisionSphere,
|
|
() => IsCurrentRecord(record)
|
|
&& record.WorldEntity is { } entity
|
|
&& record.LocalEntityId == entity.Id
|
|
&& (externalOwnerValid?.Invoke() ?? true));
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
return runtime;
|
|
}
|
|
|
|
public bool TryGetProjectileRuntime(
|
|
uint serverGuid,
|
|
out IRuntimeProjectile runtime)
|
|
{
|
|
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
&& record.ProjectileRuntime is { } found)
|
|
{
|
|
runtime = found;
|
|
return true;
|
|
}
|
|
|
|
runtime = null!;
|
|
return false;
|
|
}
|
|
|
|
public bool ClearProjectileRuntime(uint serverGuid)
|
|
{
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
|| record.ProjectileRuntime is null)
|
|
return false;
|
|
|
|
_physics.ClearProjectile(record.Canonical);
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
return true;
|
|
}
|
|
|
|
public void SetEffectProfile(uint serverGuid, ILiveEntityEffectProfile profile)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(profile);
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record))
|
|
throw new InvalidOperationException(
|
|
$"Cannot bind an effect profile before live entity 0x{serverGuid:X8} exists.");
|
|
record.EffectProfile = profile;
|
|
}
|
|
|
|
public bool TryGetEffectProfile(
|
|
uint serverGuid,
|
|
out ILiveEntityEffectProfile profile)
|
|
{
|
|
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
&& record.EffectProfile is { } found)
|
|
{
|
|
profile = found;
|
|
return true;
|
|
}
|
|
|
|
profile = null!;
|
|
return false;
|
|
}
|
|
|
|
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
|
|
_directory.TryGetSnapshot(guid, out spawn);
|
|
|
|
public bool TryApplyObjDesc(ObjDescEvent.Parsed update, out WorldSession.EntitySpawn accepted)
|
|
=> _entityObjects.TryApplyObjDesc(
|
|
update,
|
|
canonical =>
|
|
{
|
|
if (_projections.TryGet(
|
|
canonical,
|
|
out LiveEntityRecord? record))
|
|
{
|
|
record.NoteObjDescProjectionSynchronization();
|
|
}
|
|
},
|
|
out accepted);
|
|
|
|
public bool TryApplyPickup(PickupEvent.Parsed update, out WorldSession.EntitySpawn accepted)
|
|
=> _entityObjects.TryApplyPickup(
|
|
update,
|
|
AcknowledgeCelllessCanonicalCommit,
|
|
out accepted);
|
|
|
|
public bool TryApplyCreateParent(CreateParentUpdate update, out WorldSession.EntitySpawn accepted)
|
|
=> _entityObjects.TryApplyCreateParent(
|
|
update,
|
|
acknowledgeProjection: null,
|
|
out accepted);
|
|
|
|
public bool TryApplyParent(ParentEvent.Parsed update, out WorldSession.EntitySpawn accepted)
|
|
=> _entityObjects.TryApplyParent(
|
|
update,
|
|
acknowledgeProjection: null,
|
|
out accepted);
|
|
|
|
internal bool CommitStagedParent(
|
|
ParentAttachmentRelation relation,
|
|
out WorldSession.EntitySpawn accepted)
|
|
=> _entityObjects.TryCommitParent(
|
|
relation,
|
|
acknowledgeProjection: null,
|
|
out accepted);
|
|
|
|
/// <summary>
|
|
/// Commits retail <c>CPhysicsObj::set_parent</c>'s cell-less edge only
|
|
/// after the parent accepted the child (PartArray + holding-location
|
|
/// validation). The POSITION_TS is consumed before this step, matching
|
|
/// retail; invalid parent relationships retain the old world projection.
|
|
/// </summary>
|
|
internal bool CommitAcceptedParentCellless(
|
|
LiveEntityRecord record,
|
|
ulong positionAuthorityVersion)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
return _entityObjects.CommitAcceptedParentCellless(
|
|
record.Canonical,
|
|
positionAuthorityVersion,
|
|
AcknowledgeCelllessCanonicalCommit);
|
|
}
|
|
|
|
internal bool CommitAcceptedParentCellless(
|
|
RuntimeEntityRecord canonical,
|
|
ulong positionAuthorityVersion)
|
|
{
|
|
return _entityObjects.CommitAcceptedParentCellless(
|
|
canonical,
|
|
positionAuthorityVersion,
|
|
AcknowledgeCelllessCanonicalCommit);
|
|
}
|
|
|
|
public bool TryApplyMotion(
|
|
WorldSession.EntityMotionUpdate update,
|
|
bool retainPayload,
|
|
out WorldSession.EntitySpawn accepted,
|
|
out AcceptedPhysicsTimestamps timestamps)
|
|
=> _entityObjects.TryApplyMotion(
|
|
update,
|
|
retainPayload,
|
|
acknowledgeProjection: null,
|
|
out accepted,
|
|
out timestamps);
|
|
|
|
public bool TryApplyVector(VectorUpdate.Parsed update, out WorldSession.EntitySpawn accepted)
|
|
=> _entityObjects.TryApplyVector(
|
|
update,
|
|
acknowledgeProjection: null,
|
|
out accepted);
|
|
|
|
public bool TryApplyState(SetState.Parsed update, out WorldSession.EntitySpawn accepted)
|
|
=> TryApplyState(update, out accepted, out _);
|
|
|
|
public bool TryApplyState(
|
|
SetState.Parsed update,
|
|
out WorldSession.EntitySpawn accepted,
|
|
out RetailPhysicsStateTransition transition)
|
|
=> _entityObjects.TryApplyState(
|
|
update,
|
|
(canonical, _) =>
|
|
{
|
|
if (_projections.TryGet(
|
|
canonical,
|
|
out LiveEntityRecord? record))
|
|
{
|
|
RefreshPresentation(record);
|
|
}
|
|
},
|
|
out accepted,
|
|
out transition);
|
|
|
|
/// <summary>
|
|
/// Retail parent Hidden directly toggles each attached child's NoDraw bit
|
|
/// without consuming the child's STATE_TS or replacing its raw wire state.
|
|
/// </summary>
|
|
public bool SetAttachedChildNoDraw(uint childServerGuid, bool noDraw)
|
|
{
|
|
if (!_projections.TryGetCurrent(childServerGuid, out LiveEntityRecord? record))
|
|
return false;
|
|
return _entityObjects.CommitChildNoDraw(
|
|
record.Canonical,
|
|
noDraw,
|
|
_ => RefreshPresentation(record));
|
|
}
|
|
|
|
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)
|
|
{
|
|
bool projectionRequiresTeleportHook =
|
|
_directory.TryGetActive(
|
|
update.Guid,
|
|
out RuntimeEntityRecord canonical)
|
|
&& (canonical.FullCellId == 0u
|
|
|| !_projections.TryGet(
|
|
canonical,
|
|
out LiveEntityRecord? projection)
|
|
|| !projection.IsSpatiallyProjected
|
|
|| !projection.IsSpatiallyVisible);
|
|
return _entityObjects.TryApplyPosition(
|
|
update,
|
|
isLocalPlayer,
|
|
forcePositionRotation,
|
|
currentLocalVelocity,
|
|
projectionRequiresTeleportHook,
|
|
acknowledgeProjection: null,
|
|
out disposition,
|
|
out accepted,
|
|
out timestamps);
|
|
}
|
|
|
|
public bool IsFreshTeleportStart(uint localPlayerGuid, ushort teleportSequence) =>
|
|
_directory.IsFreshTeleportStart(localPlayerGuid, teleportSequence);
|
|
|
|
/// <summary>
|
|
/// Retail <c>CPhysicsObj::update_object</c> (<c>0x00515D40</c>) runs root
|
|
/// object tick only while the object has visible world-cell membership and
|
|
/// is not Frozen. Hidden is deliberately not a broad tick stop: retail
|
|
/// skips PartArray animation and physics, but still applies the
|
|
/// PositionManager offset and advances targeting/movement/script/particle
|
|
/// owners. A pickup or parent transition keeps
|
|
/// script/effect owners but clears the cell and withdraws the root
|
|
/// projection until a fresh Position re-enters it.
|
|
/// </summary>
|
|
public RetailObjectClockDisposition GetRootObjectClockDisposition(
|
|
uint serverGuid)
|
|
{
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
|| record.ProjectionKind is not LiveEntityProjectionKind.World
|
|
|| !record.IsSpatiallyProjected
|
|
|| !record.IsSpatiallyVisible
|
|
|| record.FullCellId == 0)
|
|
{
|
|
return RetailObjectClockDisposition.Suspend;
|
|
}
|
|
|
|
return (record.FinalPhysicsState & PhysicsStateFlags.Frozen) != 0
|
|
? RetailObjectClockDisposition.Suspend
|
|
: RetailObjectClockDisposition.Advance;
|
|
}
|
|
|
|
public bool ShouldAdvanceRootRuntime(uint serverGuid) =>
|
|
GetRootObjectClockDisposition(serverGuid)
|
|
is RetailObjectClockDisposition.Advance;
|
|
|
|
internal bool IsCurrentPositionAuthority(
|
|
LiveEntityRecord record,
|
|
ulong authorityVersion) =>
|
|
_projections.TryGetCurrent(record.ServerGuid, out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record)
|
|
&& current.PositionAuthorityVersion == authorityVersion;
|
|
|
|
internal bool IsCurrentPositionAuthority(
|
|
RuntimeEntityRecord canonical,
|
|
ulong authorityVersion) =>
|
|
_directory.IsCurrent(canonical)
|
|
&& canonical.PositionAuthorityVersion == authorityVersion;
|
|
|
|
internal bool IsCurrentStateAuthority(
|
|
LiveEntityRecord record,
|
|
ulong authorityVersion) =>
|
|
_projections.TryGetCurrent(record.ServerGuid, out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record)
|
|
&& current.StateAuthorityVersion == authorityVersion;
|
|
|
|
internal bool IsCurrentVectorAuthority(
|
|
LiveEntityRecord record,
|
|
ulong authorityVersion) =>
|
|
_projections.TryGetCurrent(record.ServerGuid, out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record)
|
|
&& current.VectorAuthorityVersion == authorityVersion;
|
|
|
|
internal bool IsCurrentVelocityAuthority(
|
|
LiveEntityRecord record,
|
|
ulong authorityVersion) =>
|
|
_projections.TryGetCurrent(record.ServerGuid, out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record)
|
|
&& current.VelocityAuthorityVersion == authorityVersion;
|
|
|
|
internal bool IsCurrentVelocityAuthority(
|
|
RuntimeEntityRecord canonical,
|
|
ulong authorityVersion) =>
|
|
_directory.IsCurrent(canonical)
|
|
&& canonical.VelocityAuthorityVersion == authorityVersion;
|
|
|
|
internal bool IsCurrentMovementAuthority(
|
|
LiveEntityRecord record,
|
|
ulong authorityVersion) =>
|
|
_projections.TryGetCurrent(record.ServerGuid, out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record)
|
|
&& current.MovementAuthorityVersion == authorityVersion;
|
|
|
|
internal bool IsCurrentObjDescAuthority(
|
|
LiveEntityRecord record,
|
|
ulong authorityVersion) =>
|
|
_projections.TryGetCurrent(record.ServerGuid, out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record)
|
|
&& current.ObjDescAuthorityVersion == authorityVersion;
|
|
|
|
/// <summary>
|
|
/// Copies the canonical loaded root-object workset used by retail
|
|
/// <c>CPhysics::UseTime</c>. The record reference is the incarnation token;
|
|
/// callers must revalidate it after callbacks that can delete or rebucket.
|
|
/// </summary>
|
|
internal void CopySpatialRootObjectRecordsTo(List<LiveEntityRecord> destination)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(destination);
|
|
destination.Clear();
|
|
_physics.CopySpatialRootsTo(_spatialRootCanonicalScratch);
|
|
for (int index = 0;
|
|
index < _spatialRootCanonicalScratch.Count;
|
|
index++)
|
|
{
|
|
RuntimeEntityRecord canonical =
|
|
_spatialRootCanonicalScratch[index];
|
|
if (_projections.TryGet(
|
|
canonical,
|
|
out LiveEntityRecord? current)
|
|
&& HasSpatialRuntimeProjection(current))
|
|
{
|
|
destination.Add(current);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal bool IsCurrentSpatialRootObject(LiveEntityRecord record) =>
|
|
IsCurrentRecord(record)
|
|
&& _physics.IsSpatialRoot(record.Canonical)
|
|
&& HasSpatialRuntimeProjection(record);
|
|
|
|
internal bool IsCurrentSpatialAnimation(
|
|
LiveEntityRecord record,
|
|
ILiveEntityAnimationRuntime animation) =>
|
|
IsCurrentSpatialRootObject(record)
|
|
&& ReferenceEquals(record.AnimationRuntime, animation)
|
|
&& record.ProjectionKey is { } key
|
|
&& _spatialAnimations.TryGetValue(key, out var indexed)
|
|
&& ReferenceEquals(indexed, animation);
|
|
|
|
internal bool IsCurrentSpatialAnimation(
|
|
uint localEntityId,
|
|
ILiveEntityAnimationRuntime animation) =>
|
|
_projections.TryGetByLocalId(localEntityId, out LiveEntityRecord record)
|
|
&& IsCurrentSpatialAnimation(record, animation);
|
|
|
|
/// <summary>
|
|
/// Returns whether <paramref name="animation"/> is still the canonical
|
|
/// animation component of this logical entity incarnation. Motion
|
|
/// completion belongs to that CPhysicsObj/PartArray lifetime, not to the
|
|
/// transient render-bucket projection used by per-frame presentation.
|
|
/// </summary>
|
|
internal bool IsCurrentAnimationOwner(
|
|
LiveEntityRecord record,
|
|
ILiveEntityAnimationRuntime animation) =>
|
|
IsCurrentRecord(record)
|
|
&& ReferenceEquals(record.AnimationRuntime, animation);
|
|
|
|
/// <summary>
|
|
/// Copies the currently spatial animation owners through the concrete
|
|
/// dictionary enumerator. This keeps per-frame traversal allocation-free;
|
|
/// exposing the workset as <see cref="IReadOnlyDictionary{TKey,TValue}"/>
|
|
/// would box its enumerator at a hot call site.
|
|
/// </summary>
|
|
internal void CopySpatialAnimationRuntimesTo<TAnimation>(
|
|
List<KeyValuePair<uint, TAnimation>> destination)
|
|
where TAnimation : class, ILiveEntityAnimationRuntime
|
|
{
|
|
ArgumentNullException.ThrowIfNull(destination);
|
|
destination.Clear();
|
|
foreach ((RuntimeEntityKey key, ILiveEntityAnimationRuntime animation)
|
|
in _spatialAnimations)
|
|
{
|
|
if (animation is TAnimation typed)
|
|
{
|
|
destination.Add(
|
|
new KeyValuePair<uint, TAnimation>(
|
|
key.LocalEntityId,
|
|
typed));
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void CopySpatialAnimationLocalIdsTo(HashSet<uint> destination)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(destination);
|
|
destination.Clear();
|
|
foreach (RuntimeEntityKey key in _spatialAnimations.Keys)
|
|
destination.Add(key.LocalEntityId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Commits retail <c>CPhysicsObj::set_velocity</c> (0x005113F0) to the
|
|
/// canonical body for this exact incarnation. The body and the retained
|
|
/// object clock are one CPhysicsObj: a non-Static inactive-to-active edge
|
|
/// rebases both together, while Static preserves its prior Active state.
|
|
/// </summary>
|
|
internal bool TryCommitAuthoritativeVelocity(
|
|
LiveEntityRecord record,
|
|
PhysicsBody body,
|
|
Vector3 velocity,
|
|
double currentTime) =>
|
|
TryCommitAuthoritativeVectorCore(
|
|
record,
|
|
body,
|
|
velocity,
|
|
angularVelocity: null,
|
|
currentTime);
|
|
|
|
/// <summary>
|
|
/// Commits the paired retail VectorUpdate writes
|
|
/// (<c>set_velocity</c>, then <c>set_omega</c>) to one canonical body.
|
|
/// Validation is atomic: malformed wire vectors cannot partially mutate
|
|
/// the live object.
|
|
/// </summary>
|
|
internal bool TryCommitAuthoritativeVector(
|
|
LiveEntityRecord record,
|
|
PhysicsBody body,
|
|
Vector3 velocity,
|
|
Vector3 angularVelocity,
|
|
double currentTime) =>
|
|
TryCommitAuthoritativeVectorCore(
|
|
record,
|
|
body,
|
|
velocity,
|
|
angularVelocity,
|
|
currentTime);
|
|
|
|
private bool TryCommitAuthoritativeVectorCore(
|
|
LiveEntityRecord record,
|
|
PhysicsBody body,
|
|
Vector3 velocity,
|
|
Vector3? angularVelocity,
|
|
double currentTime)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
ArgumentNullException.ThrowIfNull(body);
|
|
bool ProjectionIsCurrent() =>
|
|
_projections.TryGetCurrent(
|
|
record.ServerGuid,
|
|
out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record)
|
|
&& ReferenceEquals(current.PhysicsBody, body);
|
|
return _physics.TryCommitAuthoritativeVector(
|
|
record.Canonical,
|
|
body,
|
|
velocity,
|
|
angularVelocity,
|
|
currentTime,
|
|
ProjectionIsCurrent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies the currently spatial remote-motion owners into caller-owned
|
|
/// reusable storage. The record reference is the incarnation token: a
|
|
/// delete and GUID reuse cannot make an older snapshot address the newer
|
|
/// object.
|
|
/// </summary>
|
|
internal void CopySpatialRemoteMotionRecordsTo(List<LiveEntityRecord> destination)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(destination);
|
|
destination.Clear();
|
|
_physics.CopySpatialRemotesTo(_spatialRemoteCanonicalScratch);
|
|
for (int index = 0;
|
|
index < _spatialRemoteCanonicalScratch.Count;
|
|
index++)
|
|
{
|
|
RuntimeEntityRecord canonical =
|
|
_spatialRemoteCanonicalScratch[index];
|
|
if (_projections.TryGet(
|
|
canonical,
|
|
out LiveEntityRecord? record)
|
|
&& HasSpatialRuntimeProjection(record))
|
|
{
|
|
destination.Add(record);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal bool IsCurrentSpatialRemoteMotion(
|
|
LiveEntityRecord record,
|
|
IRuntimeRemoteMotion runtime) =>
|
|
IsCurrentRecord(record)
|
|
&& _physics.IsSpatialRemote(record.Canonical, runtime)
|
|
&& HasSpatialRuntimeProjection(record);
|
|
|
|
/// <summary>
|
|
/// Copies only projectile owners that currently have a loaded world-cell
|
|
/// projection. Pending projectiles retain their logical runtime but impose
|
|
/// no per-frame scan cost.
|
|
/// </summary>
|
|
internal void CopySpatialProjectileRecordsTo(List<LiveEntityRecord> destination)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(destination);
|
|
destination.Clear();
|
|
|
|
_physics.CopySpatialProjectilesTo(_spatialRootCanonicalScratch);
|
|
for (int i = 0; i < _spatialRootCanonicalScratch.Count; i++)
|
|
{
|
|
RuntimeEntityRecord canonical = _spatialRootCanonicalScratch[i];
|
|
if (_projections.TryGet(canonical, out LiveEntityRecord? record)
|
|
&& HasSpatialRuntimeProjection(record))
|
|
{
|
|
destination.Add(record);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal bool IsCurrentSpatialProjectile(
|
|
LiveEntityRecord record,
|
|
IRuntimeProjectile runtime) =>
|
|
IsCurrentRecord(record)
|
|
&& _physics.IsSpatialProjectile(record.Canonical, runtime)
|
|
&& HasSpatialRuntimeProjection(record);
|
|
|
|
public bool IsHidden(uint serverGuid) =>
|
|
_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
&& (record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
|
|
|
|
/// <summary>
|
|
/// Claims the one logical top-level spawn notification for this object
|
|
/// incarnation. Spatial re-entry must restore presentation without replaying
|
|
/// plugin/event registration. An attached-only child claims nothing until it
|
|
/// first becomes a top-level world projection.
|
|
/// </summary>
|
|
public bool TryMarkWorldSpawnPublished(uint serverGuid)
|
|
{
|
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
|
|| record.WorldEntity is null
|
|
|| record.ProjectionKind is not LiveEntityProjectionKind.World
|
|
|| record.WorldSpawnPublished)
|
|
return false;
|
|
record.WorldSpawnPublished = true;
|
|
return true;
|
|
}
|
|
|
|
internal bool TryMarkInitialHydrationCompleted(
|
|
LiveEntityRecord expectedRecord,
|
|
ulong expectedCreateIntegrationVersion)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
if (!IsCurrentRecord(expectedRecord)
|
|
|| expectedRecord.CreateIntegrationVersion != expectedCreateIntegrationVersion
|
|
|| expectedRecord.WorldEntity is null
|
|
|| !expectedRecord.ResourcesRegistered)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
expectedRecord.InitialHydrationCompleted = true;
|
|
return true;
|
|
}
|
|
|
|
internal bool TryMarkCreateProjectionSynchronizationPending(
|
|
LiveEntityRecord expectedRecord,
|
|
ulong expectedCreateIntegrationVersion)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
if (!IsCurrentCreateIntegration(
|
|
expectedRecord,
|
|
expectedCreateIntegrationVersion))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
expectedRecord.CreateProjectionSynchronizationPending = true;
|
|
return true;
|
|
}
|
|
|
|
internal bool TryCompleteCreateProjectionSynchronization(
|
|
LiveEntityRecord expectedRecord,
|
|
ulong expectedCreateIntegrationVersion)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
if (!IsCurrentCreateIntegration(
|
|
expectedRecord,
|
|
expectedCreateIntegrationVersion))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
expectedRecord.CreateProjectionSynchronizationPending = false;
|
|
return true;
|
|
}
|
|
|
|
internal bool TryBeginProjectionHydration(
|
|
LiveEntityRecord expectedRecord,
|
|
ulong? expectedCreateIntegrationVersion)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
if (!IsCurrentRecord(expectedRecord)
|
|
|| (expectedCreateIntegrationVersion is { } expected
|
|
&& expectedRecord.CreateIntegrationVersion != expected))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (expectedRecord.ProjectionHydrationInProgress)
|
|
{
|
|
if (expectedRecord.CreateIntegrationVersion
|
|
!= expectedRecord.ProjectionHydrationCreateVersion)
|
|
{
|
|
expectedRecord.ProjectionHydrationRetryRequested = true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
expectedRecord.ProjectionHydrationInProgress = true;
|
|
expectedRecord.ProjectionHydrationCreateVersion =
|
|
expectedRecord.CreateIntegrationVersion;
|
|
expectedRecord.ProjectionHydrationRetryRequested = false;
|
|
return true;
|
|
}
|
|
|
|
internal bool EndProjectionHydration(LiveEntityRecord expectedRecord)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
bool retry = IsCurrentRecord(expectedRecord)
|
|
&& (expectedRecord.ProjectionHydrationRetryRequested
|
|
|| expectedRecord.CreateIntegrationVersion
|
|
!= expectedRecord.ProjectionHydrationCreateVersion);
|
|
if (retry)
|
|
{
|
|
// Persist the exact replay obligation at the point version drift
|
|
// is observed. The stale projection body may be unwinding through
|
|
// an exception and therefore never reach the controller's next
|
|
// retry-loop iteration.
|
|
expectedRecord.CreateProjectionSynchronizationPending = true;
|
|
}
|
|
expectedRecord.ProjectionHydrationInProgress = false;
|
|
expectedRecord.ProjectionHydrationCreateVersion = 0UL;
|
|
expectedRecord.ProjectionHydrationRetryRequested = false;
|
|
return retry;
|
|
}
|
|
|
|
internal bool TryBeginAppearanceHydration(
|
|
LiveEntityRecord expectedRecord,
|
|
ulong expectedObjDescAuthorityVersion)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
if (!expectedRecord.AppearanceProjectionSynchronizationPending
|
|
|| !IsCurrentObjDescAuthority(
|
|
expectedRecord,
|
|
expectedObjDescAuthorityVersion))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
expectedRecord.AppearanceProjectionSynchronizationPending = true;
|
|
if (expectedRecord.AppearanceHydrationInProgress)
|
|
{
|
|
expectedRecord.AppearanceHydrationRetryRequested = true;
|
|
return false;
|
|
}
|
|
|
|
expectedRecord.AppearanceHydrationInProgress = true;
|
|
expectedRecord.AppearanceHydrationVersion =
|
|
expectedObjDescAuthorityVersion;
|
|
expectedRecord.AppearanceHydrationRetryRequested = false;
|
|
return true;
|
|
}
|
|
|
|
internal bool EndAppearanceHydration(LiveEntityRecord expectedRecord)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
bool retry = IsCurrentRecord(expectedRecord)
|
|
&& (expectedRecord.AppearanceHydrationRetryRequested
|
|
|| expectedRecord.ObjDescAuthorityVersion
|
|
!= expectedRecord.AppearanceHydrationVersion);
|
|
expectedRecord.AppearanceHydrationInProgress = false;
|
|
expectedRecord.AppearanceHydrationVersion = 0UL;
|
|
expectedRecord.AppearanceHydrationRetryRequested = false;
|
|
return retry;
|
|
}
|
|
|
|
internal bool TryCompleteAppearanceProjectionSynchronization(
|
|
LiveEntityRecord expectedRecord,
|
|
ulong expectedObjDescAuthorityVersion)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
if (!expectedRecord.AppearanceProjectionSynchronizationPending
|
|
|| !IsCurrentObjDescAuthority(
|
|
expectedRecord,
|
|
expectedObjDescAuthorityVersion))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
expectedRecord.AppearanceProjectionSynchronizationPending = false;
|
|
return true;
|
|
}
|
|
|
|
internal bool IsCurrentCreateIntegration(
|
|
LiveEntityRecord expectedRecord,
|
|
ulong expectedCreateIntegrationVersion) =>
|
|
IsCurrentRecord(expectedRecord)
|
|
&& expectedRecord.CreateIntegrationVersion == expectedCreateIntegrationVersion;
|
|
|
|
/// <summary>
|
|
/// Retires only the graphical sidecar for an exact Runtime-owned
|
|
/// incarnation. The Runtime generation-reset transaction retains and
|
|
/// completes the canonical tombstone after this acknowledgement returns.
|
|
/// </summary>
|
|
internal void RetireGenerationProjection(
|
|
RuntimeEntityRecord canonical)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(canonical);
|
|
if (_isClearing || _isRegisteringResources)
|
|
{
|
|
throw new InvalidOperationException(
|
|
_isClearing
|
|
? "Live entity projection teardown is already in progress."
|
|
: "Live entity projection teardown cannot begin inside atomic resource registration.");
|
|
}
|
|
|
|
LiveEntityRecord? record = null;
|
|
if (_projections.TryGet(canonical, out LiveEntityRecord active))
|
|
{
|
|
if (!_projections.RemoveActive(active))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Exact App projection for 0x{canonical.ServerGuid:X8}/{canonical.Incarnation} could not be retired during generation reset.");
|
|
}
|
|
_projections.RetainTeardown(active);
|
|
record = active;
|
|
}
|
|
else if (_projections.TryGetTeardown(
|
|
canonical,
|
|
out LiveEntityRecord retained))
|
|
{
|
|
record = retained;
|
|
}
|
|
|
|
if (record is null)
|
|
return;
|
|
|
|
_isClearing = true;
|
|
try
|
|
{
|
|
TearDownRecord(record, completeCanonical: false);
|
|
_projections.ReleaseTeardown(record);
|
|
}
|
|
finally
|
|
{
|
|
_isClearing = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes the App-only projection boundary after Runtime has retired
|
|
/// every exact canonical incarnation. No canonical owner is mutated here.
|
|
/// </summary>
|
|
internal void CompleteGenerationProjectionRetirement()
|
|
{
|
|
if (_projections.ActiveCount != 0
|
|
|| _projections.TeardownCount != 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Live entity projection storage has not converged at the generation-reset boundary.");
|
|
}
|
|
|
|
_spatialAnimations.Clear();
|
|
_physics.ClearSpatialWorksets();
|
|
_projections.ClearConverged();
|
|
_spatial.ClearLiveEntityLifetimeState();
|
|
_sessionClearPendingFinalization = false;
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
if (_isClearing || _isRegisteringResources)
|
|
{
|
|
throw new InvalidOperationException(
|
|
_isClearing
|
|
? "Live entity session teardown is already in progress."
|
|
: "Live entity session teardown cannot begin inside atomic resource registration.");
|
|
}
|
|
|
|
_isClearing = true;
|
|
_sessionClearPendingFinalization = true;
|
|
IReadOnlyList<RuntimeEntityRecord> retiredCanonicals =
|
|
_entityObjects.BeginSessionClear();
|
|
List<Exception>? failures = null;
|
|
try
|
|
{
|
|
foreach (RuntimeEntityRecord canonical in retiredCanonicals)
|
|
{
|
|
if (_projections.TryGet(
|
|
canonical,
|
|
out LiveEntityRecord? record))
|
|
{
|
|
if (!_projections.RemoveActive(record))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Exact App projection for 0x{record.ServerGuid:X8}/{record.Generation} could not be retired during session clear.");
|
|
}
|
|
RetainTeardownRecord(record);
|
|
}
|
|
else
|
|
{
|
|
TearDownCanonicalOnly(canonical);
|
|
}
|
|
}
|
|
|
|
foreach (LiveEntityRecord record in _projections.TeardownRecords.ToArray())
|
|
{
|
|
if (!_projections.IsRetainedTeardown(record))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// A re-entrant Clear can observe the tombstone currently being
|
|
// unregistered by its caller. Leave it queued; the outer
|
|
// teardown's ReleaseTeardownRecord finalizes the session once
|
|
// it unwinds successfully.
|
|
if (record.TeardownInProgress)
|
|
continue;
|
|
try
|
|
{
|
|
TearDownRecord(record);
|
|
ReleaseTeardownRecord(record);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= new List<Exception>()).Add(error);
|
|
}
|
|
}
|
|
|
|
CompleteSessionClearIfConverged();
|
|
}
|
|
finally
|
|
{
|
|
_isClearing = false;
|
|
}
|
|
|
|
if (failures is not null)
|
|
throw new AggregateException("One or more live entities failed session teardown.", failures);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Drains teardown tombstones whose first unregister attempt failed. This
|
|
/// runs on the update thread after the callback stack that deferred the
|
|
/// removal has unwound; successful steps are not replayed.
|
|
/// </summary>
|
|
public int RetryPendingTeardowns()
|
|
{
|
|
if (_projections.TeardownCount == 0
|
|
|| _isClearing
|
|
|| _isRegisteringResources
|
|
|| _logicalTeardownDepth != 0)
|
|
return 0;
|
|
|
|
int completed = 0;
|
|
List<Exception>? failures = null;
|
|
foreach (LiveEntityRecord record in _projections.TeardownRecords.ToArray())
|
|
{
|
|
if (!_projections.IsRetainedTeardown(record))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (record.TeardownInProgress)
|
|
continue;
|
|
|
|
_logicalTeardownDepth++;
|
|
try
|
|
{
|
|
try
|
|
{
|
|
TearDownRecord(record);
|
|
ReleaseTeardownRecord(record);
|
|
completed++;
|
|
if (!_directory.TryGetActive(record.ServerGuid, out _)
|
|
&& !HasPendingTeardown(record.ServerGuid)
|
|
&& !_directory.TryGetSnapshot(record.ServerGuid, out _))
|
|
{
|
|
_spatial.ForgetLiveEntity(record.ServerGuid);
|
|
}
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= new List<Exception>()).Add(error);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_logicalTeardownDepth--;
|
|
}
|
|
}
|
|
|
|
if (failures is not null)
|
|
throw new AggregateException("One or more pending live-entity teardowns failed again.", failures);
|
|
return completed;
|
|
}
|
|
|
|
private void RefreshRecord(
|
|
uint guid,
|
|
WorldSession.EntitySpawn accepted,
|
|
bool refreshPosition = false)
|
|
{
|
|
if (!_directory.TryGetActive(guid, out RuntimeEntityRecord canonical))
|
|
return;
|
|
_directory.RefreshSnapshot(canonical, accepted, refreshPosition);
|
|
}
|
|
|
|
private ulong AdvanceLifetimeMutation(uint serverGuid)
|
|
=> _directory.AdvanceLifetimeMutation(serverGuid);
|
|
|
|
private ulong CurrentLifetimeMutation(uint serverGuid) =>
|
|
_directory.CurrentLifetimeMutation(serverGuid);
|
|
|
|
private static RuntimeEntityKey RequireProjectionKey(
|
|
LiveEntityRecord record) =>
|
|
record.ProjectionKey ?? record.Canonical.Key
|
|
?? throw new InvalidOperationException(
|
|
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} has no materialized projection key.");
|
|
|
|
private bool IsCurrentProjectionOperation(
|
|
uint serverGuid,
|
|
LiveEntityRecord record,
|
|
ulong projectionOperation) =>
|
|
_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record)
|
|
&& record.ProjectionMutationVersion == projectionOperation;
|
|
|
|
private static void ThrowAfterCommittedProjectionChange(
|
|
uint serverGuid,
|
|
Exception? spatialNotificationFailure,
|
|
Exception? runtimeNotificationFailure)
|
|
{
|
|
if (spatialNotificationFailure is not null
|
|
&& runtimeNotificationFailure is not null)
|
|
{
|
|
throw new AggregateException(
|
|
$"Projection change for live entity 0x{serverGuid:X8} committed, but spatial and runtime observers failed.",
|
|
spatialNotificationFailure,
|
|
runtimeNotificationFailure);
|
|
}
|
|
|
|
Exception? failure = spatialNotificationFailure ?? runtimeNotificationFailure;
|
|
if (failure is not null)
|
|
ExceptionDispatchInfo.Capture(failure).Throw();
|
|
}
|
|
|
|
private void AcknowledgeCelllessCanonicalCommit(
|
|
RuntimeEntityRecord canonical)
|
|
{
|
|
if (!_directory.IsCurrent(canonical)
|
|
|| !_projections.TryGet(
|
|
canonical,
|
|
out LiveEntityRecord? record))
|
|
{
|
|
return;
|
|
}
|
|
|
|
SynchronizePhysicsBodyActiveState(record);
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
}
|
|
|
|
internal bool IsCurrentRecord(LiveEntityRecord record) =>
|
|
_projections.TryGetCurrent(record.ServerGuid, out LiveEntityRecord? current)
|
|
&& ReferenceEquals(current, record);
|
|
|
|
private static bool HasSpatialRuntimeProjection(LiveEntityRecord record) =>
|
|
record.WorldEntity is not null
|
|
&& record.ProjectionKind is LiveEntityProjectionKind.World
|
|
&& record.IsSpatiallyProjected
|
|
&& record.IsSpatiallyVisible
|
|
&& record.FullCellId != 0;
|
|
|
|
private void RefreshSpatialRuntimeIndexes(LiveEntityRecord record)
|
|
{
|
|
bool current = IsCurrentRecord(record);
|
|
bool spatial = current && HasSpatialRuntimeProjection(record);
|
|
if (record.ProjectionKey is not { } key)
|
|
{
|
|
if (record.WorldEntity is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Materialized live entity 0x{record.ServerGuid:X8}/{record.Generation} has no exact projection key.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
_physics.AcknowledgeSpatialProjection(record.Canonical, spatial);
|
|
|
|
RefreshSpatialPresentationIndexes(record, current, spatial, key);
|
|
}
|
|
|
|
private void RefreshSpatialPresentationIndexes(LiveEntityRecord record)
|
|
{
|
|
bool current = IsCurrentRecord(record);
|
|
bool spatial = current && HasSpatialRuntimeProjection(record);
|
|
if (record.ProjectionKey is not { } key)
|
|
{
|
|
if (record.WorldEntity is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Materialized live entity 0x{record.ServerGuid:X8}/{record.Generation} has no exact projection key.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
RefreshSpatialPresentationIndexes(record, current, spatial, key);
|
|
}
|
|
|
|
private void RefreshSpatialPresentationIndexes(
|
|
LiveEntityRecord record,
|
|
bool current,
|
|
bool spatial,
|
|
RuntimeEntityKey key)
|
|
{
|
|
|
|
if (record.WorldEntity is not null)
|
|
{
|
|
if (spatial && record.AnimationRuntime is { } animation)
|
|
{
|
|
_spatialAnimations[key] = animation;
|
|
}
|
|
else if (current
|
|
|| (record.AnimationRuntime is { } retainedAnimation
|
|
&& _spatialAnimations.TryGetValue(key, out var indexedAnimation)
|
|
&& ReferenceEquals(indexedAnimation, retainedAnimation)))
|
|
{
|
|
_spatialAnimations.Remove(key);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private void RemoveSpatialRuntimeIndexes(LiveEntityRecord record)
|
|
{
|
|
if (record.ProjectionKey is not { } key)
|
|
return;
|
|
|
|
_physics.RemoveSpatialProjection(record.Canonical);
|
|
|
|
if (record.WorldEntity is not null
|
|
&& _spatialAnimations.TryGetValue(key, out var indexedAnimation)
|
|
&& ReferenceEquals(indexedAnimation, record.AnimationRuntime))
|
|
{
|
|
_spatialAnimations.Remove(key);
|
|
}
|
|
|
|
}
|
|
|
|
private void OnSpatialVisibilityChanged(RuntimeEntityKey key, bool visible)
|
|
{
|
|
// GpuWorldState serializes re-entrant visibility callbacks. A callback
|
|
// may delete an incarnation and materialize the same server GUID before
|
|
// an older queued edge drains. Apply an edge only while it still
|
|
// matches current spatial truth; otherwise it belongs to the displaced
|
|
// projection and must not mutate the replacement generation.
|
|
if (_spatial.IsLiveEntityProjectionResident(key) != visible)
|
|
return;
|
|
if (!_projections.TryGet(
|
|
key,
|
|
out LiveEntityRecord? record)
|
|
|| record.WorldEntity is not { } entity
|
|
|| entity.Id != key.LocalEntityId)
|
|
return;
|
|
|
|
uint serverGuid = record.ServerGuid;
|
|
bool wasVisible = record.IsSpatiallyVisible;
|
|
if (RequireProjectionKey(record) != key)
|
|
return;
|
|
if (_presentationOnlySpatialMutationDepth.ContainsKey(key))
|
|
{
|
|
record.IsSpatiallyVisible = visible;
|
|
RefreshSpatialPresentationIndexes(record);
|
|
RefreshPresentation(record);
|
|
return;
|
|
}
|
|
bool wasOrdinaryRoot = _physics.IsSpatialRoot(record.Canonical);
|
|
record.IsSpatiallyVisible = visible;
|
|
bool isOrdinaryRoot = record.ProjectionKind is LiveEntityProjectionKind.World
|
|
&& record.IsSpatiallyProjected
|
|
&& visible
|
|
&& record.FullCellId != 0;
|
|
if (_rebucketingGuid != serverGuid && wasOrdinaryRoot != isOrdinaryRoot)
|
|
{
|
|
if (isOrdinaryRoot)
|
|
record.ResetObjectClockForEnterWorld(
|
|
(record.FinalPhysicsState & PhysicsStateFlags.Static) != 0);
|
|
else
|
|
record.SuspendObjectClock();
|
|
SynchronizePhysicsBodyActiveState(record);
|
|
}
|
|
RefreshSpatialRuntimeIndexes(record);
|
|
RefreshPresentation(record);
|
|
if (_rebucketingGuid != serverGuid && wasVisible != visible)
|
|
PublishProjectionVisibilityChanged(record, visible);
|
|
}
|
|
|
|
private void OnRuntimePhysicsCellCommitted(
|
|
RuntimePhysicsCellCommit commit)
|
|
{
|
|
if (commit.Record.SpatialAuthorityVersion
|
|
!= commit.SpatialAuthorityVersion
|
|
|| !_directory.IsCurrent(commit.Record)
|
|
|| !_projections.TryGet(
|
|
commit.Record,
|
|
out LiveEntityRecord? projection)
|
|
|| projection.WorldEntity is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RebucketLiveEntity(
|
|
commit.Record.ServerGuid,
|
|
commit.FullCellId);
|
|
}
|
|
|
|
private static void SynchronizePhysicsBodyActiveState(
|
|
LiveEntityRecord record)
|
|
{
|
|
if (record.PhysicsBody is not { } body)
|
|
return;
|
|
if (record.ObjectClock.IsActive)
|
|
body.TransientState |= TransientStateFlags.Active;
|
|
else
|
|
body.TransientState &= ~TransientStateFlags.Active;
|
|
}
|
|
|
|
private void PublishProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
|
|
{
|
|
Delegate[] subscribers = ProjectionVisibilityChanged?.GetInvocationList()
|
|
?? Array.Empty<Delegate>();
|
|
List<Exception>? failures = null;
|
|
for (int i = 0; i < subscribers.Length; i++)
|
|
{
|
|
try
|
|
{
|
|
((Action<LiveEntityRecord, bool>)subscribers[i])(record, visible);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= new List<Exception>()).Add(error);
|
|
}
|
|
}
|
|
|
|
if (failures is not null)
|
|
{
|
|
throw new AggregateException(
|
|
$"One or more projection observers failed for live entity 0x{record.ServerGuid:X8}.",
|
|
failures);
|
|
}
|
|
}
|
|
|
|
private void RefreshPresentation(LiveEntityRecord record)
|
|
{
|
|
if (record.WorldEntity is not { } entity)
|
|
return;
|
|
|
|
PhysicsStateFlags state = record.FinalPhysicsState;
|
|
bool residenceVisible = record.MaterializationResidence is not
|
|
LiveEntityMaterializationResidence.AwaitRuntimePlacement
|
|
|| record.IsSpatiallyProjected;
|
|
entity.IsDrawVisible = residenceVisible
|
|
&& (state & (PhysicsStateFlags.NoDraw | PhysicsStateFlags.Hidden)) == 0;
|
|
|
|
bool interactionVisible = record.IsSpatiallyVisible
|
|
&& record.ProjectionKind is LiveEntityProjectionKind.World
|
|
&& (state & PhysicsStateFlags.Hidden) == 0;
|
|
if (interactionVisible)
|
|
_projections.SetVisible(record, true);
|
|
else
|
|
_projections.SetVisible(record, false);
|
|
}
|
|
|
|
private void RetainTeardownRecord(LiveEntityRecord record)
|
|
{
|
|
_directory.RetainTeardown(record.Canonical);
|
|
_projections.RetainTeardown(record);
|
|
}
|
|
|
|
private Exception? RetirePriorProjection(
|
|
RuntimeEntityRecord canonical)
|
|
{
|
|
if (!_projections.TryGet(
|
|
canonical,
|
|
out LiveEntityRecord? projection))
|
|
{
|
|
return _entityObjects.RetireCanonicalOnly(canonical);
|
|
}
|
|
|
|
if (!_projections.RemoveActive(projection))
|
|
{
|
|
return new InvalidOperationException(
|
|
$"Exact App projection for 0x{canonical.ServerGuid:X8}/{canonical.Incarnation} could not be retired.");
|
|
}
|
|
|
|
RetainTeardownRecord(projection);
|
|
_logicalTeardownDepth++;
|
|
try
|
|
{
|
|
try
|
|
{
|
|
TearDownRecord(projection);
|
|
ReleaseTeardownRecord(projection);
|
|
return null;
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
return error;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_logicalTeardownDepth--;
|
|
}
|
|
}
|
|
|
|
private void TearDownCanonicalOnly(RuntimeEntityRecord canonical)
|
|
{
|
|
Exception? failure = _entityObjects.RetireCanonicalOnly(canonical);
|
|
if (failure is not null)
|
|
ExceptionDispatchInfo.Capture(failure).Throw();
|
|
}
|
|
|
|
private void ReleaseTeardownRecord(LiveEntityRecord record)
|
|
{
|
|
_projections.ReleaseTeardown(record);
|
|
_directory.ReleaseTeardown(record.Canonical);
|
|
CompleteSessionClearIfConverged();
|
|
}
|
|
|
|
private bool HasPendingTeardown(uint serverGuid) =>
|
|
_directory.HasPendingTeardown(serverGuid);
|
|
|
|
private void CompleteSessionClearIfConverged()
|
|
{
|
|
if (!_sessionClearPendingFinalization
|
|
|| !_entityObjects.CompleteSessionClearIfConverged())
|
|
{
|
|
return;
|
|
}
|
|
|
|
_spatialAnimations.Clear();
|
|
_physics.ClearSpatialWorksets();
|
|
_projections.ClearConverged();
|
|
_spatial.ClearLiveEntityLifetimeState();
|
|
_sessionClearPendingFinalization = false;
|
|
}
|
|
|
|
private void TearDownRecord(
|
|
LiveEntityRecord record,
|
|
bool completeCanonical = true)
|
|
{
|
|
if (record.TeardownInProgress)
|
|
throw new InvalidOperationException(
|
|
$"Live entity 0x{record.ServerGuid:X8} teardown is already in progress.");
|
|
|
|
record.TeardownInProgress = true;
|
|
List<Exception>? failures = null;
|
|
bool TryCleanup(Action cleanup)
|
|
{
|
|
try
|
|
{
|
|
cleanup();
|
|
return true;
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= new List<Exception>()).Add(error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
// Remove frame-workset membership before arbitrary callbacks. The
|
|
// logical component dictionaries and WorldEntity remain intact as
|
|
// a retryable tombstone until every external cleanup acknowledges
|
|
// success.
|
|
RemoveSpatialRuntimeIndexes(record);
|
|
_physics.ClearProjectile(record.Canonical);
|
|
|
|
if (!record.RuntimeComponentsTeardownCompleted)
|
|
{
|
|
record.RuntimeComponentsTeardownCompleted =
|
|
TryCleanup(() => _runtimeComponentLifecycle.TearDown(record));
|
|
}
|
|
|
|
if (record.WorldEntity is { } entity)
|
|
{
|
|
if (!record.SpatialProjectionTeardownCompleted)
|
|
{
|
|
record.SpatialProjectionTeardownCompleted =
|
|
TryCleanup(() => _spatial.RemoveLiveEntityProjection(entity));
|
|
}
|
|
if (record.ResourcesRegistered
|
|
&& TryCleanup(() => _resources.Unregister(entity)))
|
|
{
|
|
record.ResourcesRegistered = false;
|
|
}
|
|
}
|
|
|
|
if (failures is not null)
|
|
{
|
|
throw new AggregateException(
|
|
$"Live entity 0x{record.ServerGuid:X8} teardown failed.",
|
|
failures);
|
|
}
|
|
|
|
if (record.WorldEntity is not null)
|
|
_ = RequireProjectionKey(record);
|
|
if (completeCanonical)
|
|
{
|
|
_entityObjects.CompleteProjectionRetirement(
|
|
record.Canonical);
|
|
}
|
|
|
|
record.AnimationRuntime = null;
|
|
record.EffectProfile = null;
|
|
record.IsSpatiallyProjected = false;
|
|
record.IsSpatiallyVisible = false;
|
|
record.WorldSpawnPublished = false;
|
|
record.InitialHydrationCompleted = false;
|
|
record.CreateProjectionSynchronizationPending = false;
|
|
record.ProjectionHydrationInProgress = false;
|
|
record.ProjectionHydrationCreateVersion = 0UL;
|
|
record.ProjectionHydrationRetryRequested = false;
|
|
record.AppearanceProjectionSynchronizationPending = false;
|
|
record.AppearanceHydrationInProgress = false;
|
|
record.AppearanceHydrationVersion = 0UL;
|
|
record.AppearanceHydrationRetryRequested = false;
|
|
record.WorldEntity = null;
|
|
}
|
|
finally
|
|
{
|
|
record.TeardownInProgress = false;
|
|
}
|
|
}
|
|
|
|
}
|