Cutover slice C0 (docs/plans/2026-08-02-placement-cutover.md): the seam work that lets C3 flip hosts onto a complete receipt stream instead of growing one mid-cutover. The executor's Released exit now publishes an acknowledge-only ExecutorCompleted receipt through the one placement projection stream — registered before observer dispatch, correlated to the full execution receipt, reaped exactly once on acknowledgement/ discard/session-clear, and counted in the convergence ledger. All three production placement sinks acknowledge-and-ignore the new kind via early returns proven behavior-preserving for every existing kind; without them the first such receipt at cutover would permanently wedge the exact-head FIFO behind sinks that return false. Provably inert today: the publisher has no production caller. Execute's live inputs now derive from Runtime's own owners bound at GameRuntime construction: UsePositionFromServer is retail's exact autonomy_level != 2 (CommandInterpreter::UsePositionFromServer 0x006B3B40, startup-only knob), and PlayerDistance uses the live movement controller's position with a null-safe fallback to the caller struct — never a fabricated origin. TryPrepareAndSubmitAuthoredPlacement chains the prepared-collision Setup read through PrepareMover to submission with zero validation-semantics changes. TryCommitParent and CommitWithdrawal gain the sibling cancellation flow (residence + ordinary placement family); TryCommitParent deliberately omits LeaveWorld — retail's set_parent performs its single gated leave_world (0x00515A90) and a second would have no counterpart. Not fully dormant: the two cancellation fixes change Runtime paths production already calls (today as no-op-adjacent hardening, since nothing upstream begins a residence yet); everything else is reachable only by tests. Reviewed: retail-conformance PASS + architecture/ adversarial PASS after one fix round (sink wedge, completion-receipt lifecycle, null-controller distance). Runtime 921/921; complete Release solution 10,716 passed / 4 intentional skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2297 lines
88 KiB
C#
2297 lines
88 KiB
C#
using System.Collections.Immutable;
|
|
using System.Numerics;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Runtime.Physics;
|
|
|
|
namespace AcDream.Runtime.Entities;
|
|
|
|
public readonly record struct RuntimeEntityRegistrationResult(
|
|
InboundCreateResult Inbound,
|
|
RuntimeEntityRecord? Canonical,
|
|
bool LogicalRegistrationCreated,
|
|
bool ReplacedExistingGeneration,
|
|
Exception? PriorGenerationCleanupFailure = null,
|
|
bool DeferredForParent = false);
|
|
|
|
public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
|
int ActiveEntityCount,
|
|
int TeardownEntityCount,
|
|
int ClaimedLocalIdCount,
|
|
int AcceptedSnapshotCount,
|
|
int UnresolvedParentRelationCount,
|
|
int DeferredParentCreateCount,
|
|
int StagedParentRelationCount,
|
|
int RecoveryParentRelationCount,
|
|
int CommittedParentRelationCount,
|
|
int ObjectCount,
|
|
int ContainerCount,
|
|
int ContainerProjectionCount,
|
|
int EquipmentOwnerCount,
|
|
int PendingMoveCount,
|
|
int InitialCreateResidenceLeaseCount,
|
|
int InitialCreateExecutorProgressCount,
|
|
int StreamSubscriberCount,
|
|
int PlacementStreamSubscriberCount,
|
|
long StreamDispatchFailureCount,
|
|
bool HasLastStreamDispatchFailure,
|
|
int PendingDispatchCount,
|
|
bool IsDispatching,
|
|
bool IsSessionClearInProgress,
|
|
bool IsDisposed,
|
|
/// <summary>Round 5 R5-1: pending queue-by-parent-GUID accepted relations (see <see cref="ParentAttachmentState.DeferredAcceptedRelationCount"/>).</summary>
|
|
int DeferredAcceptedRelationCount = 0,
|
|
/// <summary>Round 5 R5-3: mirrors StreamDispatchFailureCount/HasLastStreamDispatchFailure for the executor's contained-replay failure surface. Diagnostic only - like its stream precedent, NOT gated by <see cref="IsConverged"/>.</summary>
|
|
long ReplayFailureCount = 0,
|
|
bool HasLastReplayFailure = false,
|
|
/// <summary>
|
|
/// F2: outstanding <see cref="RuntimeInitialCreateContinuationExecutor"/>
|
|
/// token-to-receipt correlation entries - one per completed drain whose
|
|
/// ExecutorCompleted receipt a host has not yet acknowledged. Gated by
|
|
/// <see cref="IsConverged"/>, mirroring
|
|
/// <see cref="RuntimeSetPositionOwnershipSnapshot.PendingProjectionAcknowledgementCount"/>'s
|
|
/// existing "unacknowledged receipt is outstanding debt" shape for the
|
|
/// SAME underlying receipt stream - unlike ReplayFailureCount above,
|
|
/// this is NOT a diagnostic-only counter.
|
|
/// </summary>
|
|
int PendingCompletionReceiptCount = 0)
|
|
{
|
|
public bool IsConverged =>
|
|
IsDisposed
|
|
&& ActiveEntityCount == 0
|
|
&& TeardownEntityCount == 0
|
|
&& ClaimedLocalIdCount == 0
|
|
&& AcceptedSnapshotCount == 0
|
|
&& UnresolvedParentRelationCount == 0
|
|
&& DeferredParentCreateCount == 0
|
|
&& DeferredAcceptedRelationCount == 0
|
|
&& StagedParentRelationCount == 0
|
|
&& RecoveryParentRelationCount == 0
|
|
&& CommittedParentRelationCount == 0
|
|
&& ObjectCount == 0
|
|
&& ContainerCount == 0
|
|
&& ContainerProjectionCount == 0
|
|
&& EquipmentOwnerCount == 0
|
|
&& PendingMoveCount == 0
|
|
&& InitialCreateResidenceLeaseCount == 0
|
|
&& InitialCreateExecutorProgressCount == 0
|
|
&& PendingCompletionReceiptCount == 0
|
|
&& StreamSubscriberCount == 0
|
|
&& PlacementStreamSubscriberCount == 0
|
|
&& PendingDispatchCount == 0
|
|
&& !IsDispatching
|
|
&& !IsSessionClearInProgress;
|
|
}
|
|
|
|
/// <summary>
|
|
/// One exact DeleteObject acceptance issued by a
|
|
/// <see cref="RuntimeEntityObjectLifetime"/>. The graphical host retires the
|
|
/// active identity before returning this token to the Runtime owner for the
|
|
/// retained-object mutation, preserving retail callback order.
|
|
/// </summary>
|
|
public sealed class RuntimeEntityDeleteAcceptance
|
|
{
|
|
internal RuntimeEntityDeleteAcceptance(
|
|
RuntimeEntityObjectLifetime owner,
|
|
DeleteObject.Parsed delete,
|
|
RuntimeEntityRecord? retiredCanonical,
|
|
bool removeRetainedObject)
|
|
{
|
|
Owner = owner;
|
|
Delete = delete;
|
|
RetiredCanonical = retiredCanonical;
|
|
RemoveRetainedObject = removeRetainedObject;
|
|
}
|
|
|
|
internal RuntimeEntityObjectLifetime Owner { get; }
|
|
internal bool Completed { get; set; }
|
|
public DeleteObject.Parsed Delete { get; }
|
|
public RuntimeEntityRecord? RetiredCanonical { get; }
|
|
public bool RemoveRetainedObject { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Presentation-free ownership root for one session's canonical entity and
|
|
/// retained object lifetimes. Graphical and direct hosts borrow these exact
|
|
/// instances; they never allocate a second directory or object table.
|
|
/// </summary>
|
|
public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|
{
|
|
private bool _sessionClearInProgress;
|
|
private bool _disposed;
|
|
|
|
public RuntimeEntityObjectLifetime(
|
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
|
|
TimeProvider? timeProvider = null,
|
|
IGameRuntimeClock? gameClock = null)
|
|
{
|
|
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
|
Physics = new RuntimePhysicsState(
|
|
Entities,
|
|
timeProvider: timeProvider,
|
|
gameClock: gameClock);
|
|
Objects = new ClientObjectTable();
|
|
// AP-129 (Campaign P Slice P4 review fix, 2026-07-30): the physics
|
|
// entry-restriction gate (ObjectInfo.CheckEntryRestrictions) resolves
|
|
// a restricted cell's owner/guest list through the SAME live
|
|
// ClientObjectTable every other subsystem borrows from this owner —
|
|
// never a second table. Without this, every restricted cell fails
|
|
// closed for everyone (see PhysicsEngine.Objects).
|
|
Physics.Engine.Objects = Objects;
|
|
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
|
EntityView = views.Entities;
|
|
InventoryView = views.Inventory;
|
|
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
|
|
Physics.SetPosition.BindEventStream(Events);
|
|
InitialCreateResidences = new RuntimeInitialCreateResidenceState(
|
|
Entities,
|
|
Physics.SetPosition);
|
|
InitialCreateExecution = new RuntimeInitialCreateContinuationExecutor(
|
|
Entities,
|
|
InitialCreateResidences,
|
|
Physics,
|
|
Events,
|
|
(spawn, isLocalPlayer) =>
|
|
RegisterEntityWithInitialResidence(spawn, isLocalPlayer),
|
|
(canonical, version, spawn, replaceGeneration) =>
|
|
ApplyAcceptedSpawn(canonical, version, spawn, replaceGeneration));
|
|
// Round 3 B3: every residence retirement path - not only the
|
|
// executor's own DiscardProgress calls - must converge the
|
|
// executor's progress AND its separately-tracked pending
|
|
// continuation placement token. This class owns both sides of the
|
|
// relationship, so it binds the delegate here rather than the
|
|
// residence state referencing the executor type directly.
|
|
InitialCreateResidences.BindRetirementNotification(
|
|
key => InitialCreateExecution.DiscardProgress(key));
|
|
// F2: reaps the executor's completion-receipt correlation entry
|
|
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
|
// correlates - mirrors the residence-retirement binding immediately
|
|
// above.
|
|
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
|
|
(key, sequence) =>
|
|
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
|
Placements = new RuntimePlacementProjectionChannel(
|
|
Events,
|
|
Physics.SetPosition);
|
|
}
|
|
|
|
internal RuntimeEntityObjectLifetime(
|
|
PhysicsDataCache physicsDataCache,
|
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
|
|
TimeProvider? timeProvider = null,
|
|
IGameRuntimeClock? gameClock = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(physicsDataCache);
|
|
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
|
Physics = new RuntimePhysicsState(
|
|
Entities,
|
|
physicsDataCache,
|
|
timeProvider,
|
|
gameClock);
|
|
Objects = new ClientObjectTable();
|
|
Physics.Engine.Objects = Objects;
|
|
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
|
EntityView = views.Entities;
|
|
InventoryView = views.Inventory;
|
|
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
|
|
Physics.SetPosition.BindEventStream(Events);
|
|
InitialCreateResidences = new RuntimeInitialCreateResidenceState(
|
|
Entities,
|
|
Physics.SetPosition);
|
|
InitialCreateExecution = new RuntimeInitialCreateContinuationExecutor(
|
|
Entities,
|
|
InitialCreateResidences,
|
|
Physics,
|
|
Events,
|
|
(spawn, isLocalPlayer) =>
|
|
RegisterEntityWithInitialResidence(spawn, isLocalPlayer),
|
|
(canonical, version, spawn, replaceGeneration) =>
|
|
ApplyAcceptedSpawn(canonical, version, spawn, replaceGeneration));
|
|
// Round 3 B3: every residence retirement path - not only the
|
|
// executor's own DiscardProgress calls - must converge the
|
|
// executor's progress AND its separately-tracked pending
|
|
// continuation placement token. This class owns both sides of the
|
|
// relationship, so it binds the delegate here rather than the
|
|
// residence state referencing the executor type directly.
|
|
InitialCreateResidences.BindRetirementNotification(
|
|
key => InitialCreateExecution.DiscardProgress(key));
|
|
// F2: reaps the executor's completion-receipt correlation entry
|
|
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
|
// correlates - mirrors the residence-retirement binding immediately
|
|
// above.
|
|
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
|
|
(key, sequence) =>
|
|
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
|
Placements = new RuntimePlacementProjectionChannel(
|
|
Events,
|
|
Physics.SetPosition);
|
|
}
|
|
|
|
internal RuntimeEntityObjectLifetime(
|
|
PhysicsEngine physicsEngine,
|
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
|
|
TimeProvider? timeProvider = null,
|
|
IGameRuntimeClock? gameClock = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(physicsEngine);
|
|
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
|
Physics = new RuntimePhysicsState(
|
|
Entities,
|
|
physicsEngine,
|
|
timeProvider,
|
|
gameClock);
|
|
Objects = new ClientObjectTable();
|
|
Physics.Engine.Objects = Objects;
|
|
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
|
EntityView = views.Entities;
|
|
InventoryView = views.Inventory;
|
|
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
|
|
Physics.SetPosition.BindEventStream(Events);
|
|
InitialCreateResidences = new RuntimeInitialCreateResidenceState(
|
|
Entities,
|
|
Physics.SetPosition);
|
|
InitialCreateExecution = new RuntimeInitialCreateContinuationExecutor(
|
|
Entities,
|
|
InitialCreateResidences,
|
|
Physics,
|
|
Events,
|
|
(spawn, isLocalPlayer) =>
|
|
RegisterEntityWithInitialResidence(spawn, isLocalPlayer),
|
|
(canonical, version, spawn, replaceGeneration) =>
|
|
ApplyAcceptedSpawn(canonical, version, spawn, replaceGeneration));
|
|
// Round 3 B3: every residence retirement path - not only the
|
|
// executor's own DiscardProgress calls - must converge the
|
|
// executor's progress AND its separately-tracked pending
|
|
// continuation placement token. This class owns both sides of the
|
|
// relationship, so it binds the delegate here rather than the
|
|
// residence state referencing the executor type directly.
|
|
InitialCreateResidences.BindRetirementNotification(
|
|
key => InitialCreateExecution.DiscardProgress(key));
|
|
// F2: reaps the executor's completion-receipt correlation entry
|
|
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
|
// correlates - mirrors the residence-retirement binding immediately
|
|
// above.
|
|
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
|
|
(key, sequence) =>
|
|
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
|
Placements = new RuntimePlacementProjectionChannel(
|
|
Events,
|
|
Physics.SetPosition);
|
|
}
|
|
|
|
public RuntimeEntityDirectory Entities { get; }
|
|
public RuntimePhysicsState Physics { get; }
|
|
public ClientObjectTable Objects { get; }
|
|
public IRuntimeEntityView EntityView { get; }
|
|
public IRuntimeInventoryView InventoryView { get; }
|
|
public RuntimeEntityObjectEventStream Events { get; }
|
|
public RuntimePlacementProjectionChannel Placements { get; }
|
|
internal RuntimeInitialCreateResidenceState InitialCreateResidences
|
|
{ get; }
|
|
internal RuntimeInitialCreateContinuationExecutor InitialCreateExecution
|
|
{ get; }
|
|
|
|
public RuntimeEntityObjectOwnershipSnapshot CaptureOwnership()
|
|
{
|
|
ParentAttachmentState parents = Entities.ParentAttachments;
|
|
RuntimeInitialCreateResidenceOwnershipSnapshot initialResidence =
|
|
InitialCreateResidences.CaptureOwnership();
|
|
return new RuntimeEntityObjectOwnershipSnapshot(
|
|
Entities.Count,
|
|
Entities.PendingTeardownCount,
|
|
Entities.ClaimedLocalIdCount,
|
|
Entities.Snapshots.Count,
|
|
parents.UnresolvedRelationCount,
|
|
parents.DeferredCreateCount,
|
|
parents.StagedRelationCount,
|
|
parents.RecoveryRelationCount,
|
|
parents.CommittedRelationCount,
|
|
Objects.ObjectCount,
|
|
Objects.ContainerCount,
|
|
Objects.ContainerProjectionCount,
|
|
Objects.EquipmentOwnerCount,
|
|
Objects.PendingMoveCount,
|
|
initialResidence.ActiveLeaseCount
|
|
+ initialResidence.PendingAdoptionCount,
|
|
InitialCreateExecution.ProgressCount,
|
|
Events.SubscriberCount,
|
|
Events.PlacementSubscriberCount,
|
|
Events.DispatchFailureCount,
|
|
Events.LastDispatchFailure is not null,
|
|
Events.PendingDispatchCount,
|
|
Events.IsDispatching,
|
|
_sessionClearInProgress,
|
|
_disposed,
|
|
parents.DeferredAcceptedRelationCount,
|
|
InitialCreateExecution.ReplayFailureCount,
|
|
InitialCreateExecution.LastReplayFailure is not null,
|
|
InitialCreateExecution.PendingCompletionReceiptCount);
|
|
}
|
|
|
|
public void BindEventContext(
|
|
Func<RuntimeGenerationToken> generation,
|
|
Func<ulong> frameNumber)
|
|
{
|
|
EnsureNotDisposed();
|
|
Events.BindContext(generation, frameNumber);
|
|
Placements.BindGeneration(generation);
|
|
InitialCreateResidences.BindGeneration(generation);
|
|
InitialCreateExecution.BindGeneration(generation);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C0-2: forwards to <see cref="RuntimeInitialCreateContinuationExecutor.BindLiveInputs"/>,
|
|
/// the same fan-out shape <see cref="BindEventContext"/> already uses for
|
|
/// generation binding. Separate from <see cref="BindEventContext"/>
|
|
/// because <c>GameRuntime</c> constructs <c>RuntimeCharacterState</c>/
|
|
/// <c>RuntimeLocalPlayerMovementState</c> (the real source owners) AFTER
|
|
/// this lifetime, so the live-input bind necessarily happens at a later
|
|
/// point in <c>GameRuntime</c>'s construction sequence than the
|
|
/// generation bind.
|
|
/// </summary>
|
|
public void BindLiveInputs(
|
|
Func<bool> usePositionFromServer,
|
|
Func<Vector3?> localPlayerPosition)
|
|
{
|
|
EnsureNotDisposed();
|
|
InitialCreateExecution.BindLiveInputs(
|
|
usePositionFromServer, localPlayerPosition);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Owns the presentation-free half of retail's CreateObject lifetime
|
|
/// transaction. An attached graphical host may synchronously retire the
|
|
/// displaced projection through <paramref name="retirePriorProjection"/>;
|
|
/// a direct host omits it and Runtime retires canonical-only state.
|
|
/// </summary>
|
|
public RuntimeEntityRegistrationResult RegisterEntity(
|
|
WorldSession.EntitySpawn incoming,
|
|
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection = null) =>
|
|
RegisterEntityCore(
|
|
incoming,
|
|
beginInitialResidence: false,
|
|
isLocalPlayer: false,
|
|
retirePriorProjection);
|
|
|
|
internal RuntimeEntityRegistrationResult RegisterEntityWithInitialResidence(
|
|
WorldSession.EntitySpawn incoming,
|
|
bool isLocalPlayer,
|
|
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection = null) =>
|
|
RegisterEntityCore(
|
|
incoming,
|
|
beginInitialResidence: true,
|
|
isLocalPlayer,
|
|
retirePriorProjection);
|
|
|
|
private RuntimeEntityRegistrationResult RegisterEntityCore(
|
|
WorldSession.EntitySpawn incoming,
|
|
bool beginInitialResidence,
|
|
bool isLocalPlayer,
|
|
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection)
|
|
{
|
|
EnsureNotDisposed();
|
|
if (_sessionClearInProgress)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"A Runtime entity cannot register while its session lifetime is clearing.");
|
|
}
|
|
if (beginInitialResidence
|
|
&& !HasConsistentCreateIdentityAndParent(incoming))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"CreateObject 0x{incoming.Guid:X8} has inconsistent instance or parent projections.");
|
|
}
|
|
if (beginInitialResidence)
|
|
incoming = RuntimeInitialCreateAdmissionFreezer.Freeze(incoming);
|
|
uint parentGuid = incoming.ParentGuid
|
|
?? incoming.Physics?.Parent?.Guid
|
|
?? 0u;
|
|
if (beginInitialResidence
|
|
&& parentGuid != 0u
|
|
&& !Entities.TryGetActive(parentGuid, out _))
|
|
{
|
|
// SmartBox::HandleCreateObject resolves a nonzero parent before
|
|
// object lookup or timestamp admission. Retain the complete raw
|
|
// CreateObject so no child gate/canonical/projection state can
|
|
// escape before the parent becomes addressable.
|
|
Entities.ParentAttachments.EnqueueDeferredCreate(
|
|
incoming,
|
|
isLocalPlayer);
|
|
return new RuntimeEntityRegistrationResult(
|
|
SupersededCreateResult(),
|
|
Canonical: null,
|
|
LogicalRegistrationCreated: false,
|
|
ReplacedExistingGeneration: false,
|
|
DeferredForParent: true);
|
|
}
|
|
CreateObjectTimestampDisposition preview =
|
|
Entities.PreviewCreateDisposition(incoming);
|
|
bool requiresFreshResidenceAdmission = preview is
|
|
CreateObjectTimestampDisposition.InitialGeneration
|
|
or CreateObjectTimestampDisposition.NewGeneration;
|
|
if (beginInitialResidence
|
|
&& requiresFreshResidenceAdmission
|
|
&& !InitialCreateResidences.CanAcceptCreate(incoming))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"CreateObject 0x{incoming.Guid:X8} cannot acquire a structurally valid initial residence lease.");
|
|
}
|
|
|
|
RuntimeEntityRecord? pendingResidenceRecord = null;
|
|
RuntimeInitialCreateResidenceLease pendingResidence = default;
|
|
bool admitIntoPendingResidence = beginInitialResidence
|
|
&& preview is CreateObjectTimestampDisposition.ExistingGeneration
|
|
&& Entities.TryGetActive(
|
|
incoming.Guid,
|
|
out pendingResidenceRecord)
|
|
&& InitialCreateResidences.TryGetTransaction(
|
|
pendingResidenceRecord,
|
|
out pendingResidence);
|
|
if (admitIntoPendingResidence
|
|
&& !InitialCreateResidences.CanEnqueue(
|
|
pendingResidenceRecord!,
|
|
pendingResidence))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"CreateObject 0x{incoming.Guid:X8} cannot append to its pending initial residence FIFO.");
|
|
}
|
|
if (admitIntoPendingResidence
|
|
&& !IsStructurallyValidDeferredCreate(incoming))
|
|
{
|
|
_ = Entities.TryGetAcceptedTimestamps(
|
|
incoming.Guid,
|
|
out AcceptedPhysicsTimestamps timestamps);
|
|
return new RuntimeEntityRegistrationResult(
|
|
new InboundCreateResult(
|
|
CreateObjectTimestampDisposition.ExistingGeneration,
|
|
pendingResidenceRecord!.Snapshot,
|
|
SameGenerationEvents: null,
|
|
timestamps),
|
|
pendingResidenceRecord,
|
|
LogicalRegistrationCreated: false,
|
|
ReplacedExistingGeneration: false);
|
|
}
|
|
|
|
InboundCreateResult result = admitIntoPendingResidence
|
|
? Entities.AcceptCreateDeferredSameGeneration(incoming)
|
|
: Entities.AcceptCreate(incoming);
|
|
if (result.Disposition
|
|
is CreateObjectTimestampDisposition.StaleGeneration)
|
|
{
|
|
return new RuntimeEntityRegistrationResult(
|
|
result,
|
|
Canonical: null,
|
|
LogicalRegistrationCreated: false,
|
|
ReplacedExistingGeneration: false);
|
|
}
|
|
|
|
ulong sessionVersion = Entities.SessionLifetimeVersion;
|
|
ulong operationVersion =
|
|
Entities.AdvanceLifetimeMutation(incoming.Guid);
|
|
|
|
if (result.Disposition
|
|
is CreateObjectTimestampDisposition.ExistingGeneration)
|
|
{
|
|
if (Entities.TryGetActive(
|
|
incoming.Guid,
|
|
out RuntimeEntityRecord retained))
|
|
{
|
|
if (admitIntoPendingResidence)
|
|
{
|
|
if (!ReferenceEquals(retained, pendingResidenceRecord)
|
|
|| !AdmitSameGenerationCreate(
|
|
retained,
|
|
pendingResidence,
|
|
incoming,
|
|
result,
|
|
isLocalPlayer))
|
|
{
|
|
throw FailInitialResidenceRegistration(
|
|
retained,
|
|
publishDeleted: true);
|
|
}
|
|
|
|
InboundCreateResult dormant = result with
|
|
{
|
|
Snapshot = retained.Snapshot,
|
|
SameGenerationEvents = null,
|
|
};
|
|
return new RuntimeEntityRegistrationResult(
|
|
dormant,
|
|
retained,
|
|
LogicalRegistrationCreated: false,
|
|
ReplacedExistingGeneration: false);
|
|
}
|
|
|
|
// Existing-generation CreateObject contributes untimestamped
|
|
// description fields here. Position/Parent/Pickup/State/etc.
|
|
// remain separate freshness-gated events and must not churn a
|
|
// pending initial placement or re-preclaim its wire cell.
|
|
Entities.RefreshSnapshot(
|
|
retained,
|
|
result.Snapshot,
|
|
refreshPosition: !beginInitialResidence);
|
|
if (!beginInitialResidence)
|
|
Entities.AdvanceCreateAuthority(retained);
|
|
PublishEntity(RuntimeEntityChange.Updated, retained);
|
|
if (!IsCurrentOperation(
|
|
incoming.Guid,
|
|
retained,
|
|
sessionVersion,
|
|
operationVersion))
|
|
{
|
|
return SupersededRegistration(
|
|
incoming.Guid,
|
|
replacedExistingGeneration: false);
|
|
}
|
|
return new RuntimeEntityRegistrationResult(
|
|
result,
|
|
retained,
|
|
LogicalRegistrationCreated: false,
|
|
ReplacedExistingGeneration: false);
|
|
}
|
|
|
|
if (Entities.TryGetTeardown(
|
|
incoming.Guid,
|
|
result.Snapshot.InstanceSequence,
|
|
out _))
|
|
{
|
|
return new RuntimeEntityRegistrationResult(
|
|
result,
|
|
Canonical: null,
|
|
LogicalRegistrationCreated: false,
|
|
ReplacedExistingGeneration: false);
|
|
}
|
|
|
|
if (beginInitialResidence
|
|
&& !InitialCreateResidences.CanAcceptCreate(result.Snapshot))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Recovered CreateObject 0x{incoming.Guid:X8} cannot acquire a structurally valid initial residence lease.");
|
|
}
|
|
|
|
RuntimeEntityRecord recovered = Entities.AddActive(result.Snapshot);
|
|
if (!InitializeAcceptedCreateResidence(
|
|
recovered,
|
|
result,
|
|
beginInitialResidence,
|
|
isLocalPlayer))
|
|
{
|
|
throw FailInitialResidenceRegistration(
|
|
recovered,
|
|
publishDeleted: false);
|
|
}
|
|
PublishEntity(RuntimeEntityChange.Registered, recovered);
|
|
if (!IsCurrentOperation(
|
|
incoming.Guid,
|
|
recovered,
|
|
sessionVersion,
|
|
operationVersion))
|
|
{
|
|
return SupersededRegistration(
|
|
incoming.Guid,
|
|
replacedExistingGeneration: false);
|
|
}
|
|
return new RuntimeEntityRegistrationResult(
|
|
result,
|
|
recovered,
|
|
LogicalRegistrationCreated: true,
|
|
ReplacedExistingGeneration: false);
|
|
}
|
|
|
|
bool replaced = Entities.RemoveActive(
|
|
incoming.Guid,
|
|
out RuntimeEntityRecord? prior);
|
|
if (result.Disposition
|
|
is CreateObjectTimestampDisposition.NewGeneration)
|
|
{
|
|
Entities.ParentAttachments.EndGeneration(
|
|
incoming.Guid,
|
|
result.Snapshot.InstanceSequence);
|
|
}
|
|
|
|
Exception? cleanupFailure = null;
|
|
if (prior is not null)
|
|
{
|
|
// Removing the active GUID is an ownership transfer, not a gap.
|
|
// Retain the exact incarnation before arbitrary synchronous
|
|
// observers can re-enter registration, reset, or disposal.
|
|
Entities.RetainTeardown(prior);
|
|
try
|
|
{
|
|
PublishEntity(RuntimeEntityChange.Deleted, prior);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
cleanupFailure = error;
|
|
}
|
|
|
|
Exception? projectionFailure = retirePriorProjection is null
|
|
? RetireCanonicalOnly(prior)
|
|
: retirePriorProjection(prior);
|
|
cleanupFailure = Combine(cleanupFailure, projectionFailure);
|
|
}
|
|
|
|
if (Entities.SessionLifetimeVersion != sessionVersion
|
|
|| Entities.CurrentLifetimeMutation(incoming.Guid)
|
|
!= operationVersion)
|
|
{
|
|
if (cleanupFailure is not null)
|
|
{
|
|
throw new AggregateException(
|
|
$"Prior incarnation of live entity 0x{incoming.Guid:X8} failed teardown while its incoming replacement was superseded.",
|
|
cleanupFailure);
|
|
}
|
|
|
|
return new RuntimeEntityRegistrationResult(
|
|
SupersededCreateResult(),
|
|
Entities.TryGetActive(
|
|
incoming.Guid,
|
|
out RuntimeEntityRecord current)
|
|
? current
|
|
: null,
|
|
LogicalRegistrationCreated: false,
|
|
ReplacedExistingGeneration: replaced);
|
|
}
|
|
|
|
RuntimeEntityRecord canonical = Entities.AddActive(result.Snapshot);
|
|
if (!InitializeAcceptedCreateResidence(
|
|
canonical,
|
|
result,
|
|
beginInitialResidence,
|
|
isLocalPlayer))
|
|
{
|
|
throw FailInitialResidenceRegistration(
|
|
canonical,
|
|
publishDeleted: false);
|
|
}
|
|
try
|
|
{
|
|
PublishEntity(RuntimeEntityChange.Registered, canonical);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
if (cleanupFailure is not null)
|
|
{
|
|
throw new AggregateException(
|
|
$"Live entity 0x{incoming.Guid:X8} registered after prior cleanup and commit observers failed.",
|
|
cleanupFailure,
|
|
error);
|
|
}
|
|
throw;
|
|
}
|
|
|
|
if (!IsCurrentOperation(
|
|
incoming.Guid,
|
|
canonical,
|
|
sessionVersion,
|
|
operationVersion))
|
|
{
|
|
if (cleanupFailure is not null)
|
|
{
|
|
throw new AggregateException(
|
|
$"Prior incarnation of live entity 0x{incoming.Guid:X8} failed teardown while its committed replacement was superseded.",
|
|
cleanupFailure);
|
|
}
|
|
|
|
return SupersededRegistration(
|
|
incoming.Guid,
|
|
replaced);
|
|
}
|
|
|
|
return new RuntimeEntityRegistrationResult(
|
|
result,
|
|
canonical,
|
|
LogicalRegistrationCreated: true,
|
|
ReplacedExistingGeneration: replaced,
|
|
cleanupFailure);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes a displaced incarnation that never crossed the graphical
|
|
/// projection acquisition edge.
|
|
/// </summary>
|
|
public Exception? RetireCanonicalOnly(RuntimeEntityRecord canonical)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(canonical);
|
|
try
|
|
{
|
|
Entities.RetainTeardown(canonical);
|
|
try
|
|
{
|
|
CompleteProjectionRetirement(canonical);
|
|
}
|
|
finally
|
|
{
|
|
Entities.ReleaseTeardown(canonical);
|
|
}
|
|
return null;
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
return error;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes the presentation acknowledgement for one retained
|
|
/// incarnation. Runtime retires every presentation-independent component
|
|
/// and releases its local identity only after the graphical host has
|
|
/// finished using those exact components for ExitWorld teardown.
|
|
/// </summary>
|
|
internal void CompleteProjectionRetirement(
|
|
RuntimeEntityRecord canonical)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(canonical);
|
|
RuntimePlacementCancellationReceipt initialCancellation =
|
|
ForgetInitialCreateResidence(canonical);
|
|
RuntimePlacementCancellationReceipt ordinaryCancellation =
|
|
Physics.SetPosition.Forget(
|
|
canonical,
|
|
releasePreparedMover: true);
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
PreferCancellation(initialCancellation, ordinaryCancellation);
|
|
Physics.CollisionReports.Forget(canonical);
|
|
Physics.RemoveSpatialProjection(canonical);
|
|
Entities.SetRemoteMotion(canonical, null);
|
|
Entities.SetRemoteMotionBindingInProgress(canonical, false);
|
|
Entities.SetProjectile(canonical, null);
|
|
Entities.SetProjectileBindingInProgress(canonical, false);
|
|
Entities.SetRequiresRemotePlacementRuntime(canonical, false);
|
|
Entities.SetPhysicsHost(canonical, null);
|
|
Entities.SetPhysicsBody(canonical, null);
|
|
Entities.SetPhysicsBodyAcquisitionInProgress(canonical, false);
|
|
Entities.SetHasPartArray(canonical, false);
|
|
Entities.ReleaseLocalId(canonical);
|
|
Physics.SetPosition.PublishCancellation(cancellation);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the retained-object half of an accepted CreateObject while the
|
|
/// exact canonical incarnation and its integration version remain current.
|
|
/// Object-table callbacks are synchronous and may re-enter entity lifetime,
|
|
/// so the acceptance predicate is checked before, during, and after apply.
|
|
/// </summary>
|
|
public bool ApplyAcceptedSpawn(
|
|
RuntimeEntityRecord canonical,
|
|
ulong expectedCreateIntegrationVersion,
|
|
WorldSession.EntitySpawn spawn,
|
|
bool replaceGeneration)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(canonical);
|
|
if (canonical.ServerGuid != spawn.Guid
|
|
|| canonical.Incarnation != spawn.InstanceSequence)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool IsCurrent() =>
|
|
Entities.IsCurrent(canonical)
|
|
&& canonical.CreateIntegrationVersion
|
|
== expectedCreateIntegrationVersion;
|
|
|
|
return IsCurrent()
|
|
&& ObjectTableWiring.ApplyEntitySpawn(
|
|
Objects,
|
|
spawn,
|
|
replaceGeneration,
|
|
IsCurrent)
|
|
&& IsCurrent();
|
|
}
|
|
|
|
public bool TryApplyObjDesc(
|
|
ObjDescEvent.Parsed update,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
|
out WorldSession.EntitySpawn accepted)
|
|
{
|
|
EnsureNotDisposed();
|
|
if (TryGetPendingInitialResidence(
|
|
update.Guid,
|
|
out RuntimeEntityRecord pending,
|
|
out RuntimeInitialCreateResidenceLease lease))
|
|
{
|
|
if (!InitialCreateResidences.CanEnqueue(pending, lease))
|
|
{
|
|
accepted = pending.Snapshot;
|
|
return false;
|
|
}
|
|
bool acceptedByGate = Entities.TryAcceptDeferredObjDesc(
|
|
update,
|
|
out _);
|
|
accepted = pending.Snapshot;
|
|
if (!acceptedByGate)
|
|
return false;
|
|
EnqueueDormant(
|
|
pending,
|
|
lease,
|
|
RuntimeInitialCreateContinuationKind.ObjDesc,
|
|
RuntimeAcceptedPositionSource.Unknown,
|
|
new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.ObjDesc,
|
|
update.Guid,
|
|
ObjDesc: update));
|
|
return true;
|
|
}
|
|
bool applied = Entities.TryApplyObjDesc(update, out accepted);
|
|
if (!applied
|
|
|| !Entities.TryGetActive(
|
|
update.Guid,
|
|
out RuntimeEntityRecord canonical))
|
|
{
|
|
return applied;
|
|
}
|
|
|
|
Entities.RefreshSnapshot(canonical, accepted);
|
|
Entities.AdvanceObjDescAuthority(canonical);
|
|
ulong authorityVersion = canonical.ObjDescAuthorityVersion;
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(canonical),
|
|
RuntimeEntityChange.Updated,
|
|
() => canonical.ObjDescAuthorityVersion == authorityVersion);
|
|
}
|
|
|
|
public bool TryApplyPickup(
|
|
PickupEvent.Parsed update,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
|
out WorldSession.EntitySpawn accepted)
|
|
{
|
|
EnsureNotDisposed();
|
|
if (TryGetPendingInitialResidence(
|
|
update.Guid,
|
|
out RuntimeEntityRecord pending,
|
|
out RuntimeInitialCreateResidenceLease lease))
|
|
{
|
|
if (!InitialCreateResidences.CanEnqueue(pending, lease))
|
|
{
|
|
accepted = pending.Snapshot;
|
|
return false;
|
|
}
|
|
bool acceptedByGate = Entities.TryAcceptDeferredPickup(
|
|
update,
|
|
out _);
|
|
accepted = pending.Snapshot;
|
|
if (!acceptedByGate)
|
|
return false;
|
|
EnqueueDormant(
|
|
pending,
|
|
lease,
|
|
RuntimeInitialCreateContinuationKind.Pickup,
|
|
RuntimeAcceptedPositionSource.Unknown,
|
|
new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.Pickup,
|
|
update.Guid,
|
|
Pickup: update));
|
|
return true;
|
|
}
|
|
bool applied = Entities.TryApplyPickup(update, out accepted);
|
|
if (!applied
|
|
|| !Entities.TryGetActive(
|
|
update.Guid,
|
|
out RuntimeEntityRecord canonical))
|
|
{
|
|
return applied;
|
|
}
|
|
|
|
Entities.RefreshSnapshot(canonical, accepted);
|
|
RuntimePlacementCancellationReceipt initialCancellation =
|
|
ForgetInitialCreateResidence(canonical);
|
|
Entities.AdvancePositionAuthority(canonical);
|
|
Physics.CollisionReports.LeaveWorld(canonical);
|
|
RuntimePlacementCancellationReceipt ordinaryCancellation =
|
|
Physics.SetPosition.Forget(canonical);
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
PreferCancellation(initialCancellation, ordinaryCancellation);
|
|
Entities.SuspendObjectClock(canonical);
|
|
Entities.SetFullCell(canonical, 0u, 0u);
|
|
Entities.ParentAttachments.EndChildProjection(update.Guid);
|
|
ulong positionVersion = canonical.PositionAuthorityVersion;
|
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(canonical),
|
|
RuntimeEntityChange.Withdrawn,
|
|
() => canonical.PositionAuthorityVersion == positionVersion
|
|
&& canonical.SpatialAuthorityVersion == spatialVersion,
|
|
cancellation);
|
|
}
|
|
|
|
public bool TryApplyCreateParent(
|
|
CreateParentUpdate update,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
|
out WorldSession.EntitySpawn accepted)
|
|
{
|
|
EnsureNotDisposed();
|
|
if (TryGetPendingInitialResidence(
|
|
update.ChildGuid,
|
|
out _,
|
|
out _))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"A CreateObject parent relation must be admitted inside its atomic same-generation Create envelope.");
|
|
}
|
|
bool applied = Entities.TryApplyCreateParent(update, out accepted);
|
|
return CommitPositionChannelUpdate(
|
|
applied,
|
|
update.ChildGuid,
|
|
accepted,
|
|
acknowledgeProjection);
|
|
}
|
|
|
|
public bool TryApplyParent(
|
|
ParentEvent.Parsed update,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
|
out WorldSession.EntitySpawn accepted)
|
|
{
|
|
EnsureNotDisposed();
|
|
if (TryGetPendingInitialResidence(
|
|
update.ChildGuid,
|
|
out RuntimeEntityRecord pending,
|
|
out RuntimeInitialCreateResidenceLease lease))
|
|
{
|
|
if (!InitialCreateResidences.CanEnqueue(pending, lease))
|
|
{
|
|
accepted = pending.Snapshot;
|
|
return false;
|
|
}
|
|
if (!Entities.TryGetActive(
|
|
update.ParentGuid,
|
|
out RuntimeEntityRecord parent)
|
|
|| parent.Incarnation != update.ParentInstanceSequence)
|
|
{
|
|
Entities.ParentAttachments.Enqueue(update);
|
|
accepted = pending.Snapshot;
|
|
return false;
|
|
}
|
|
|
|
bool acceptedByGate = Entities.TryAcceptDeferredParent(
|
|
update,
|
|
out AcceptedPhysicsTimestamps timestamps);
|
|
accepted = pending.Snapshot;
|
|
if (!acceptedByGate)
|
|
return false;
|
|
EnqueueDormant(
|
|
pending,
|
|
lease,
|
|
RuntimeInitialCreateContinuationKind.Parent,
|
|
RuntimeAcceptedPositionSource.Unknown,
|
|
new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.Parent,
|
|
update.ChildGuid,
|
|
Parent: update,
|
|
AcceptedTimestamps: timestamps));
|
|
return true;
|
|
}
|
|
bool applied = Entities.TryApplyParent(update, out accepted);
|
|
return CommitPositionChannelUpdate(
|
|
applied,
|
|
update.ChildGuid,
|
|
accepted,
|
|
acknowledgeProjection);
|
|
}
|
|
|
|
public bool TryCommitParent(
|
|
ParentAttachmentRelation relation,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
|
out WorldSession.EntitySpawn accepted)
|
|
{
|
|
EnsureNotDisposed();
|
|
bool committed = Entities.TryCommitParent(
|
|
relation.ChildGuid,
|
|
relation.ParentGuid,
|
|
relation.ParentLocation,
|
|
relation.PlacementId,
|
|
relation.ChildPositionSequence,
|
|
out accepted);
|
|
if (!committed
|
|
|| !Entities.TryGetActive(
|
|
relation.ChildGuid,
|
|
out RuntimeEntityRecord canonical))
|
|
{
|
|
return committed;
|
|
}
|
|
|
|
Entities.RefreshSnapshot(canonical, accepted);
|
|
// C0-4(a): this method had NO cancellation choke-point at all,
|
|
// leaving a residence lease or ordinary SetPosition operation
|
|
// dangling once ordinary placement traffic goes live - fixed with
|
|
// the SAME exactly-once ForgetInitialCreateResidence ->
|
|
// Physics.SetPosition.Forget -> PreferCancellation sequence every
|
|
// other commit in this family (CommitPositionChannelUpdate,
|
|
// TryApplyPickup, CommitAcceptedParentCellless, TryAcceptDelete)
|
|
// uses for THIS part of the job.
|
|
// F4 (deliberate, NOT an oversight): unlike CommitPositionChannelUpdate,
|
|
// this method does NOT also call Physics.CollisionReports.LeaveWorld.
|
|
// Retail set_parent (0x00515A90, lines 283832-283833) performs its
|
|
// single leave_world call gated behind the SAME add_child branch this
|
|
// method's staged/deferred-replay commit represents (App's
|
|
// EquippedChildRenderController realize sequence, the executor's own
|
|
// ParentRelationReplay) - a second LeaveWorld here would double-leave-
|
|
// world with no retail counterpart.
|
|
RuntimePlacementCancellationReceipt initialCancellation =
|
|
ForgetInitialCreateResidence(canonical);
|
|
RuntimePlacementCancellationReceipt ordinaryCancellation =
|
|
Physics.SetPosition.Forget(canonical);
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
PreferCancellation(initialCancellation, ordinaryCancellation);
|
|
Entities.AdvanceParentCommit(canonical);
|
|
ulong parentCommitVersion = canonical.ParentCommitVersion;
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(canonical),
|
|
RuntimeEntityChange.Updated,
|
|
() => canonical.ParentCommitVersion
|
|
== parentCommitVersion,
|
|
cancellation);
|
|
}
|
|
|
|
public bool CommitAcceptedParentCellless(
|
|
RuntimeEntityRecord canonical,
|
|
ulong positionAuthorityVersion,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(canonical);
|
|
if (!Entities.IsCurrent(canonical)
|
|
|| canonical.PositionAuthorityVersion != positionAuthorityVersion)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
RuntimePlacementCancellationReceipt initialCancellation =
|
|
ForgetInitialCreateResidence(canonical);
|
|
RuntimePlacementCancellationReceipt ordinaryCancellation =
|
|
Physics.SetPosition.Forget(canonical);
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
PreferCancellation(initialCancellation, ordinaryCancellation);
|
|
Physics.CollisionReports.LeaveWorld(canonical);
|
|
Entities.SuspendObjectClock(canonical);
|
|
Entities.SetFullCell(canonical, 0u, 0u);
|
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(canonical),
|
|
RuntimeEntityChange.Withdrawn,
|
|
() => canonical.PositionAuthorityVersion
|
|
== positionAuthorityVersion
|
|
&& canonical.SpatialAuthorityVersion == spatialVersion,
|
|
cancellation);
|
|
}
|
|
|
|
public bool TryApplyMotion(
|
|
WorldSession.EntityMotionUpdate update,
|
|
bool retainPayload,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
|
out WorldSession.EntitySpawn accepted,
|
|
out AcceptedPhysicsTimestamps timestamps)
|
|
{
|
|
EnsureNotDisposed();
|
|
if (TryGetPendingInitialResidence(
|
|
update.Guid,
|
|
out RuntimeEntityRecord pending,
|
|
out RuntimeInitialCreateResidenceLease lease))
|
|
{
|
|
if (!InitialCreateResidences.CanEnqueue(pending, lease))
|
|
{
|
|
accepted = pending.Snapshot;
|
|
timestamps = default;
|
|
return false;
|
|
}
|
|
bool payloadApplied = Entities.TryAcceptDeferredMotion(
|
|
update,
|
|
out timestamps,
|
|
out bool timestampMutation);
|
|
accepted = pending.Snapshot;
|
|
if (!payloadApplied && !timestampMutation)
|
|
return false;
|
|
EnqueueDormant(
|
|
pending,
|
|
lease,
|
|
RuntimeInitialCreateContinuationKind.Movement,
|
|
RuntimeAcceptedPositionSource.Unknown,
|
|
new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.Movement,
|
|
update.Guid,
|
|
Movement: update,
|
|
AcceptedTimestamps: timestamps,
|
|
AppliesMovementPayload: payloadApplied,
|
|
RetainMovementPayload: retainPayload,
|
|
HasTimestampMutation: timestampMutation));
|
|
return payloadApplied;
|
|
}
|
|
bool applied = Entities.TryApplyMotion(
|
|
update,
|
|
retainPayload,
|
|
out accepted,
|
|
out timestamps);
|
|
if (Entities.TryGetSnapshot(
|
|
update.Guid,
|
|
out WorldSession.EntitySpawn snapshot)
|
|
&& Entities.TryGetActive(
|
|
update.Guid,
|
|
out RuntimeEntityRecord canonical))
|
|
{
|
|
Entities.RefreshSnapshot(canonical, snapshot);
|
|
if (applied && retainPayload)
|
|
Entities.AdvanceMovementAuthority(canonical);
|
|
if (applied)
|
|
{
|
|
Entities.AdvanceMovementCommit(canonical);
|
|
ulong movementCommitVersion =
|
|
canonical.MovementCommitVersion;
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(canonical),
|
|
RuntimeEntityChange.Updated,
|
|
() => canonical.MovementCommitVersion
|
|
== movementCommitVersion);
|
|
}
|
|
|
|
acknowledgeProjection?.Invoke(canonical);
|
|
}
|
|
|
|
return applied;
|
|
}
|
|
|
|
public bool TryApplyVector(
|
|
VectorUpdate.Parsed update,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
|
out WorldSession.EntitySpawn accepted)
|
|
{
|
|
EnsureNotDisposed();
|
|
if (TryGetPendingInitialResidence(
|
|
update.Guid,
|
|
out RuntimeEntityRecord pending,
|
|
out RuntimeInitialCreateResidenceLease lease))
|
|
{
|
|
if (!IsFinite(update.Velocity)
|
|
|| !IsFinite(update.Omega)
|
|
|| !InitialCreateResidences.CanEnqueue(pending, lease))
|
|
{
|
|
accepted = pending.Snapshot;
|
|
return false;
|
|
}
|
|
bool acceptedByGate = Entities.TryAcceptDeferredVector(
|
|
update,
|
|
out _);
|
|
accepted = pending.Snapshot;
|
|
if (!acceptedByGate)
|
|
return false;
|
|
EnqueueDormant(
|
|
pending,
|
|
lease,
|
|
RuntimeInitialCreateContinuationKind.Vector,
|
|
RuntimeAcceptedPositionSource.Unknown,
|
|
new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.Vector,
|
|
update.Guid,
|
|
Vector: update));
|
|
return true;
|
|
}
|
|
bool applied = Entities.TryApplyVector(update, out accepted);
|
|
if (!applied
|
|
|| !Entities.TryGetActive(
|
|
update.Guid,
|
|
out RuntimeEntityRecord canonical))
|
|
{
|
|
return applied;
|
|
}
|
|
|
|
Entities.RefreshSnapshot(canonical, accepted);
|
|
Entities.AdvanceVectorAuthority(canonical);
|
|
ulong authorityVersion = canonical.VectorAuthorityVersion;
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(canonical),
|
|
RuntimeEntityChange.Updated,
|
|
() => canonical.VectorAuthorityVersion == authorityVersion);
|
|
}
|
|
|
|
public bool TryApplyState(
|
|
SetState.Parsed update,
|
|
Action<RuntimeEntityRecord, RetailPhysicsStateTransition>?
|
|
acknowledgeProjection,
|
|
out WorldSession.EntitySpawn accepted,
|
|
out RetailPhysicsStateTransition transition)
|
|
{
|
|
EnsureNotDisposed();
|
|
if (TryGetPendingInitialResidence(
|
|
update.Guid,
|
|
out RuntimeEntityRecord pending,
|
|
out RuntimeInitialCreateResidenceLease lease))
|
|
{
|
|
if (!InitialCreateResidences.CanEnqueue(pending, lease))
|
|
{
|
|
accepted = pending.Snapshot;
|
|
transition = default;
|
|
return false;
|
|
}
|
|
bool acceptedByGate = Entities.TryAcceptDeferredState(
|
|
update,
|
|
out _);
|
|
accepted = pending.Snapshot;
|
|
transition = default;
|
|
if (!acceptedByGate)
|
|
return false;
|
|
EnqueueDormant(
|
|
pending,
|
|
lease,
|
|
RuntimeInitialCreateContinuationKind.State,
|
|
RuntimeAcceptedPositionSource.Unknown,
|
|
new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.State,
|
|
update.Guid,
|
|
State: update));
|
|
return true;
|
|
}
|
|
bool applied = Entities.TryApplyState(update, out accepted);
|
|
transition = default;
|
|
if (!applied
|
|
|| !Entities.TryGetActive(
|
|
update.Guid,
|
|
out RuntimeEntityRecord canonical))
|
|
{
|
|
return applied;
|
|
}
|
|
|
|
Entities.RefreshSnapshot(canonical, accepted);
|
|
RetailPhysicsStateTransition preview =
|
|
RetailPhysicsStateTransitions.Apply(
|
|
canonical.FinalPhysicsState,
|
|
(PhysicsStateFlags)update.PhysicsState);
|
|
ulong priorPhysicsMutation = canonical.PhysicsStateMutationVersion;
|
|
if (preview.HiddenTransition is RetailHiddenTransition.BecameHidden)
|
|
{
|
|
Physics.CollisionReports.LeaveWorld(canonical);
|
|
if (!Entities.IsCurrent(canonical)
|
|
|| canonical.PhysicsStateMutationVersion
|
|
!= priorPhysicsMutation)
|
|
{
|
|
transition = default;
|
|
return false;
|
|
}
|
|
}
|
|
transition = Entities.ApplyRawPhysicsState(
|
|
canonical,
|
|
update.PhysicsState);
|
|
if (canonical.Key is { } key)
|
|
{
|
|
Physics.Engine.ShadowObjects.UpdatePhysicsState(
|
|
key.LocalEntityId,
|
|
(uint)canonical.FinalPhysicsState);
|
|
}
|
|
ulong stateVersion = canonical.StateAuthorityVersion;
|
|
ulong physicsMutationVersion =
|
|
canonical.PhysicsStateMutationVersion;
|
|
RetailPhysicsStateTransition committedTransition = transition;
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(
|
|
canonical,
|
|
committedTransition),
|
|
committedTransition.HiddenTransition
|
|
is RetailHiddenTransition.BecameHidden
|
|
? RuntimeEntityChange.Hidden
|
|
: RuntimeEntityChange.Updated,
|
|
() => canonical.StateAuthorityVersion == stateVersion
|
|
&& canonical.PhysicsStateMutationVersion
|
|
== physicsMutationVersion);
|
|
}
|
|
|
|
public bool TryApplyPosition(
|
|
WorldSession.EntityPositionUpdate update,
|
|
bool isLocalPlayer,
|
|
System.Numerics.Quaternion? forcePositionRotation,
|
|
System.Numerics.Vector3? currentLocalVelocity,
|
|
bool projectionRequiresTeleportHook,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
|
out PositionTimestampDisposition disposition,
|
|
out WorldSession.EntitySpawn accepted,
|
|
out AcceptedPhysicsTimestamps timestamps)
|
|
{
|
|
EnsureNotDisposed();
|
|
if (TryGetPendingInitialResidence(
|
|
update.Guid,
|
|
out RuntimeEntityRecord pendingCanonical,
|
|
out RuntimeInitialCreateResidenceLease pendingLease))
|
|
{
|
|
if (!RuntimeAuthoritativePositionRouteClassifier
|
|
.IsValidCreateWirePosition(update.Position)
|
|
|| update.Velocity is { } velocity
|
|
&& !IsFinite(velocity)
|
|
|| !InitialCreateResidences.CanEnqueue(
|
|
pendingCanonical,
|
|
pendingLease))
|
|
{
|
|
disposition = PositionTimestampDisposition.Rejected;
|
|
accepted = default;
|
|
timestamps = default;
|
|
return false;
|
|
}
|
|
|
|
bool deferredKnown = Entities.TryAcceptDeferredPosition(
|
|
update,
|
|
isLocalPlayer,
|
|
out disposition,
|
|
out timestamps,
|
|
out bool timestampMutation);
|
|
accepted = pendingCanonical.Snapshot;
|
|
if (!deferredKnown)
|
|
return false;
|
|
|
|
if (disposition is PositionTimestampDisposition.Rejected
|
|
&& !timestampMutation)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
EnqueueDormant(
|
|
pendingCanonical,
|
|
pendingLease,
|
|
RuntimeInitialCreateContinuationKind.Position,
|
|
RuntimeAcceptedPositionSource.PositionEvent,
|
|
new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.Position,
|
|
update.Guid,
|
|
Position: update,
|
|
PositionSource:
|
|
RuntimeAcceptedPositionSource.PositionEvent,
|
|
PositionDisposition: disposition,
|
|
PreviousTeleportSequence:
|
|
timestamps.PreviousTeleport,
|
|
AcceptedTimestamps: timestamps,
|
|
HasTimestampMutation: timestampMutation));
|
|
return true;
|
|
}
|
|
bool hadCanonical = Entities.TryGetActive(
|
|
update.Guid,
|
|
out RuntimeEntityRecord beforeCanonical);
|
|
uint beforeCell = beforeCanonical?.FullCellId ?? 0u;
|
|
bool wasCellless = hadCanonical && beforeCell == 0u;
|
|
bool known = Entities.TryApplyPosition(
|
|
update,
|
|
isLocalPlayer,
|
|
forcePositionRotation,
|
|
currentLocalVelocity,
|
|
out disposition,
|
|
out accepted,
|
|
out timestamps);
|
|
if (!known
|
|
|| !Entities.TryGetSnapshot(
|
|
update.Guid,
|
|
out WorldSession.EntitySpawn snapshot)
|
|
|| !Entities.TryGetActive(
|
|
update.Guid,
|
|
out RuntimeEntityRecord canonical))
|
|
{
|
|
return known;
|
|
}
|
|
|
|
bool acceptedPosition =
|
|
disposition is not PositionTimestampDisposition.Rejected;
|
|
if (disposition is PositionTimestampDisposition.Apply)
|
|
{
|
|
timestamps = timestamps with
|
|
{
|
|
TeleportHookRequired =
|
|
timestamps.TeleportAdvanced
|
|
|| wasCellless
|
|
|| projectionRequiresTeleportHook,
|
|
};
|
|
}
|
|
RuntimePlacementCancellationReceipt cancellation = default;
|
|
if (acceptedPosition)
|
|
{
|
|
cancellation = Physics.SetPosition.Forget(canonical);
|
|
}
|
|
Entities.RefreshSnapshot(
|
|
canonical,
|
|
snapshot,
|
|
refreshPosition: acceptedPosition);
|
|
if (acceptedPosition
|
|
&& ReferenceEquals(canonical, beforeCanonical))
|
|
{
|
|
Entities.AdvancePositionAuthority(canonical);
|
|
Entities.ParentAttachments.EndChildProjection(update.Guid);
|
|
}
|
|
|
|
ulong positionVersion = canonical.PositionAuthorityVersion;
|
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
|
if (!acceptedPosition)
|
|
{
|
|
acknowledgeProjection?.Invoke(canonical);
|
|
return IsExpectedCanonical(
|
|
canonical,
|
|
() => canonical.PositionAuthorityVersion == positionVersion
|
|
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
|
}
|
|
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(canonical),
|
|
beforeCell != canonical.FullCellId
|
|
? RuntimeEntityChange.Rebucketed
|
|
: RuntimeEntityChange.Updated,
|
|
() => canonical.PositionAuthorityVersion == positionVersion
|
|
&& canonical.SpatialAuthorityVersion == spatialVersion,
|
|
cancellation);
|
|
}
|
|
|
|
public bool CommitRebucket(
|
|
RuntimeEntityRecord canonical,
|
|
uint fullCellId,
|
|
uint canonicalLandblockId,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection = null)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(canonical);
|
|
if (!Entities.IsCurrent(canonical))
|
|
return false;
|
|
|
|
uint previous = canonical.FullCellId;
|
|
Entities.SetFullCell(
|
|
canonical,
|
|
fullCellId,
|
|
canonicalLandblockId);
|
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
|
if (previous == fullCellId)
|
|
{
|
|
acknowledgeProjection?.Invoke(canonical);
|
|
return IsExpectedCanonical(
|
|
canonical,
|
|
() => canonical.SpatialAuthorityVersion
|
|
== spatialVersion);
|
|
}
|
|
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(canonical),
|
|
RuntimeEntityChange.Rebucketed,
|
|
() => canonical.SpatialAuthorityVersion == spatialVersion);
|
|
}
|
|
|
|
public bool CommitWithdrawal(
|
|
RuntimeEntityRecord canonical,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection = null)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(canonical);
|
|
if (!Entities.IsCurrent(canonical))
|
|
return false;
|
|
|
|
// C0-4(b): this cancelled the initial-create residence but never the
|
|
// ORDINARY placement family (Physics.SetPosition.Forget), unlike
|
|
// TryApplyPickup/CommitAcceptedParentCellless/TryAcceptDelete, which
|
|
// all cancel both. WithdrawLiveEntityProjectionToCellless routes
|
|
// through here, so a live ordinary SetPosition/lost-cell watch could
|
|
// dangle across a withdrawal-to-cellless once ordinary placement
|
|
// traffic goes live. Fixed symmetrically with the same
|
|
// Forget/PreferCancellation pair every sibling withdrawal uses.
|
|
RuntimePlacementCancellationReceipt initialCancellation =
|
|
ForgetInitialCreateResidence(canonical);
|
|
Physics.CollisionReports.LeaveWorld(canonical);
|
|
RuntimePlacementCancellationReceipt ordinaryCancellation =
|
|
Physics.SetPosition.Forget(canonical);
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
PreferCancellation(initialCancellation, ordinaryCancellation);
|
|
Entities.SuspendObjectClock(canonical);
|
|
Entities.SetFullCell(canonical, 0u, 0u);
|
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(canonical),
|
|
RuntimeEntityChange.Withdrawn,
|
|
() => canonical.SpatialAuthorityVersion == spatialVersion,
|
|
cancellation);
|
|
}
|
|
|
|
public bool CommitChildNoDraw(
|
|
RuntimeEntityRecord canonical,
|
|
bool noDraw,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection = null)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(canonical);
|
|
if (!Entities.IsCurrent(canonical))
|
|
return false;
|
|
|
|
Entities.SetChildNoDraw(canonical, noDraw);
|
|
ulong physicsMutationVersion =
|
|
canonical.PhysicsStateMutationVersion;
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(canonical),
|
|
RuntimeEntityChange.Updated,
|
|
() => canonical.PhysicsStateMutationVersion
|
|
== physicsMutationVersion);
|
|
}
|
|
|
|
public bool RetireAfterProjectionAcquisitionFailure(
|
|
RuntimeEntityRecord canonical)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(canonical);
|
|
if (!Entities.RemoveActive(canonical))
|
|
return false;
|
|
|
|
Entities.AdvanceLifetimeMutation(canonical.ServerGuid);
|
|
Entities.RetainTeardown(canonical);
|
|
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Accepts and retires the exact canonical DeleteObject generation without
|
|
/// yet publishing the retained-object removal. This split lets a graphical
|
|
/// host tear down the exact retired projection before completing the
|
|
/// retained-object mutation while Runtime remains the only freshness,
|
|
/// identity, and canonical-lifetime authority.
|
|
/// </summary>
|
|
public bool TryAcceptDelete(
|
|
DeleteObject.Parsed delete,
|
|
bool isLocalPlayer,
|
|
bool removeRetainedObject,
|
|
out RuntimeEntityDeleteAcceptance acceptance)
|
|
{
|
|
EnsureNotDisposed();
|
|
if (!isLocalPlayer)
|
|
{
|
|
// A child whose complete raw CreateObject is waiting on a missing
|
|
// parent has no timestamp gate yet. Cancel its exact/older raw
|
|
// generation before the normal known-object delete gate returns.
|
|
Entities.ParentAttachments.CancelDeferredChildGeneration(
|
|
delete.Guid,
|
|
delete.InstanceSequence);
|
|
}
|
|
if (!Entities.TryDelete(delete, isLocalPlayer))
|
|
{
|
|
acceptance = null!;
|
|
return false;
|
|
}
|
|
|
|
Entities.AdvanceLifetimeMutation(delete.Guid);
|
|
Entities.ParentAttachments.DeleteGeneration(
|
|
delete.Guid,
|
|
delete.InstanceSequence);
|
|
|
|
RuntimeEntityRecord? retiredCanonical = null;
|
|
if (Entities.TryGetActive(
|
|
delete.Guid,
|
|
out RuntimeEntityRecord active)
|
|
&& active.Incarnation == delete.InstanceSequence
|
|
&& Entities.RemoveActive(active))
|
|
{
|
|
RuntimePlacementCancellationReceipt initialCancellation =
|
|
ForgetInitialCreateResidence(active);
|
|
Physics.CollisionReports.Forget(active);
|
|
RuntimePlacementCancellationReceipt ordinaryCancellation =
|
|
Physics.SetPosition.Forget(
|
|
active,
|
|
releasePreparedMover: true);
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
PreferCancellation(
|
|
initialCancellation,
|
|
ordinaryCancellation);
|
|
retiredCanonical = active;
|
|
Entities.RetainTeardown(active);
|
|
Physics.SetPosition.PublishCancellation(cancellation);
|
|
PublishEntity(
|
|
removeRetainedObject
|
|
? RuntimeEntityChange.Deleted
|
|
: RuntimeEntityChange.Withdrawn,
|
|
active);
|
|
}
|
|
|
|
acceptance = new RuntimeEntityDeleteAcceptance(
|
|
this,
|
|
delete,
|
|
retiredCanonical,
|
|
removeRetainedObject);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes a previously accepted delete exactly once. Marking the token
|
|
/// complete before synchronous callbacks preserves the current retry rule:
|
|
/// a throwing observer does not replay an already-committed removal.
|
|
/// </summary>
|
|
public void CompleteAcceptedDelete(
|
|
RuntimeEntityDeleteAcceptance acceptance)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(acceptance);
|
|
if (!ReferenceEquals(acceptance.Owner, this))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"A delete acceptance belongs to a different Runtime lifetime.");
|
|
}
|
|
if (acceptance.Completed)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"A delete acceptance has already been completed.");
|
|
}
|
|
|
|
acceptance.Completed = true;
|
|
if (acceptance.RemoveRetainedObject)
|
|
ObjectTableWiring.ApplyEntityDelete(Objects, acceptance.Delete);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies a delete accepted by the dormant exact-incarnation owner after
|
|
/// the active Runtime directory correctly reports no live record.
|
|
/// </summary>
|
|
public void ApplyAcceptedDormantDelete(DeleteObject.Parsed delete)
|
|
{
|
|
EnsureNotDisposed();
|
|
ObjectTableWiring.ApplyEntityDelete(Objects, delete);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears retained object state at the Runtime-owned reset stage. App
|
|
/// projection teardown remains a later acknowledged stage.
|
|
/// </summary>
|
|
public void ClearObjects()
|
|
{
|
|
EnsureNotDisposed();
|
|
Objects.Clear();
|
|
}
|
|
|
|
public IReadOnlyList<RuntimeEntityRecord> BeginSessionClear()
|
|
{
|
|
EnsureNotDisposed();
|
|
if (_sessionClearInProgress)
|
|
return Array.Empty<RuntimeEntityRecord>();
|
|
|
|
_sessionClearInProgress = true;
|
|
RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray();
|
|
InitialCreateResidences.Clear();
|
|
InitialCreateExecution.DiscardAll();
|
|
Physics.CollisionReports.LeaveWorldBatch(active);
|
|
Physics.ResetSessionPhysics();
|
|
Entities.BeginSessionClear();
|
|
foreach (RuntimeEntityRecord canonical in active)
|
|
{
|
|
Physics.SetPosition.Forget(canonical);
|
|
if (!Entities.RemoveActive(canonical))
|
|
continue;
|
|
Entities.RetainTeardown(canonical);
|
|
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
|
}
|
|
return active;
|
|
}
|
|
|
|
public IReadOnlyList<RuntimeEntityRecord>
|
|
CaptureSessionClearRetirements()
|
|
{
|
|
EnsureNotDisposed();
|
|
if (!_sessionClearInProgress)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Session-clear retirements are only available while the "
|
|
+ "canonical clear transaction is active.");
|
|
}
|
|
return Entities.TeardownRecords.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes one exact incarnation retained by
|
|
/// <see cref="BeginSessionClear"/> after the borrowed host has retired its
|
|
/// presentation projection. Failure keeps the tombstone and local
|
|
/// identity intact so the same transaction cursor can retry this exact
|
|
/// record without reconstructing the retirement set.
|
|
/// </summary>
|
|
public void CompleteSessionEntityRetirement(
|
|
RuntimeEntityRecord canonical)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(canonical);
|
|
if (!_sessionClearInProgress
|
|
|| !Entities.TryGetTeardown(
|
|
canonical.ServerGuid,
|
|
canonical.Incarnation,
|
|
out RuntimeEntityRecord retained)
|
|
|| !ReferenceEquals(retained, canonical))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Live entity 0x{canonical.ServerGuid:X8}/{canonical.Incarnation} is not retained by the active session-clear transaction.");
|
|
}
|
|
|
|
CompleteProjectionRetirement(canonical);
|
|
Entities.ReleaseTeardown(canonical);
|
|
}
|
|
|
|
public bool CompleteSessionClearIfConverged()
|
|
{
|
|
EnsureNotDisposed();
|
|
if (!_sessionClearInProgress
|
|
|| !Entities.CompleteSessionClearIfConverged())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_sessionClearInProgress = false;
|
|
return true;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
// Disposal is terminal, unlike a session reset. Quiesce borrowers
|
|
// before committing internal cleanup so no observer can re-enter a
|
|
// lifetime whose owner is being destroyed.
|
|
Events.DetachObservers();
|
|
List<Exception>? failures = null;
|
|
try
|
|
{
|
|
if (!_sessionClearInProgress)
|
|
_ = BeginSessionClear();
|
|
|
|
try
|
|
{
|
|
ClearObjects();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= []).Add(error);
|
|
}
|
|
|
|
foreach (RuntimeEntityRecord canonical
|
|
in Entities.TeardownRecords.ToArray())
|
|
{
|
|
Exception? failure = RetireCanonicalOnly(canonical);
|
|
if (failure is not null)
|
|
(failures ??= []).Add(failure);
|
|
}
|
|
|
|
if (!CompleteSessionClearIfConverged())
|
|
{
|
|
(failures ??= []).Add(new InvalidOperationException(
|
|
"Runtime entity/object disposal did not converge every canonical owner."));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_disposed = true;
|
|
Events.Dispose();
|
|
Physics.Dispose();
|
|
}
|
|
|
|
if (failures is not null)
|
|
{
|
|
throw new AggregateException(
|
|
"Runtime entity/object disposal failed to converge.",
|
|
failures);
|
|
}
|
|
}
|
|
|
|
private bool CommitPositionChannelUpdate(
|
|
bool applied,
|
|
uint guid,
|
|
WorldSession.EntitySpawn accepted,
|
|
Action<RuntimeEntityRecord>? acknowledgeProjection)
|
|
{
|
|
if (!applied
|
|
|| !Entities.TryGetActive(
|
|
guid,
|
|
out RuntimeEntityRecord canonical))
|
|
{
|
|
return applied;
|
|
}
|
|
|
|
Entities.RefreshSnapshot(canonical, accepted);
|
|
RuntimePlacementCancellationReceipt initialCancellation =
|
|
ForgetInitialCreateResidence(canonical);
|
|
Entities.AdvancePositionAuthority(canonical);
|
|
Physics.CollisionReports.LeaveWorld(canonical);
|
|
RuntimePlacementCancellationReceipt ordinaryCancellation =
|
|
Physics.SetPosition.Forget(canonical);
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
PreferCancellation(initialCancellation, ordinaryCancellation);
|
|
ulong positionVersion = canonical.PositionAuthorityVersion;
|
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
|
return AcknowledgeProjectionAndPublish(
|
|
canonical,
|
|
() => acknowledgeProjection?.Invoke(canonical),
|
|
RuntimeEntityChange.Updated,
|
|
() => canonical.PositionAuthorityVersion == positionVersion
|
|
&& canonical.SpatialAuthorityVersion == spatialVersion,
|
|
cancellation);
|
|
}
|
|
|
|
private void PublishEntity(
|
|
RuntimeEntityChange change,
|
|
RuntimeEntityRecord canonical) =>
|
|
Events.PublishEntity(change, canonical);
|
|
|
|
private bool AcknowledgeProjectionAndPublish(
|
|
RuntimeEntityRecord canonical,
|
|
Action acknowledgeProjection,
|
|
RuntimeEntityChange change,
|
|
Func<bool> matchesCommittedMutation,
|
|
RuntimePlacementCancellationReceipt cancellation = default)
|
|
{
|
|
Physics.SetPosition.PublishCancellation(cancellation);
|
|
if (!IsExpectedCanonical(canonical, matchesCommittedMutation))
|
|
return false;
|
|
try
|
|
{
|
|
acknowledgeProjection();
|
|
}
|
|
finally
|
|
{
|
|
if (IsExpectedCanonical(
|
|
canonical,
|
|
matchesCommittedMutation))
|
|
{
|
|
PublishEntity(change, canonical);
|
|
}
|
|
}
|
|
|
|
return IsExpectedCanonical(
|
|
canonical,
|
|
matchesCommittedMutation);
|
|
}
|
|
|
|
private bool IsExpectedCanonical(
|
|
RuntimeEntityRecord canonical,
|
|
Func<bool> matchesCommittedMutation) =>
|
|
Entities.IsCurrent(canonical)
|
|
&& matchesCommittedMutation();
|
|
|
|
private bool TryGetPendingInitialResidence(
|
|
uint guid,
|
|
out RuntimeEntityRecord canonical,
|
|
out RuntimeInitialCreateResidenceLease lease)
|
|
{
|
|
if (Entities.TryGetActive(guid, out canonical)
|
|
&& InitialCreateResidences.TryGetTransaction(
|
|
canonical,
|
|
out lease))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
canonical = null!;
|
|
lease = default;
|
|
return false;
|
|
}
|
|
|
|
private void EnqueueDormant(
|
|
RuntimeEntityRecord canonical,
|
|
in RuntimeInitialCreateResidenceLease prior,
|
|
RuntimeInitialCreateContinuationKind kind,
|
|
RuntimeAcceptedPositionSource positionSource,
|
|
in RuntimeInitialCreateTailAction action)
|
|
{
|
|
RuntimeInitialCreateResidenceLease retained = InitialCreateResidences
|
|
.EnqueueAccepted(
|
|
canonical,
|
|
prior,
|
|
kind,
|
|
positionSource,
|
|
ImmutableArray.Create(action));
|
|
if (!retained.IsValid)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Accepted {kind} for 0x{canonical.ServerGuid:X8}/{canonical.Incarnation} could not be retained by its initial-placement FIFO.");
|
|
}
|
|
}
|
|
|
|
private static bool IsFinite(System.Numerics.Vector3 value) =>
|
|
float.IsFinite(value.X)
|
|
&& float.IsFinite(value.Y)
|
|
&& float.IsFinite(value.Z);
|
|
|
|
private static bool HasConsistentCreateIdentityAndParent(
|
|
in WorldSession.EntitySpawn incoming)
|
|
{
|
|
if (incoming.Guid == 0u)
|
|
return false;
|
|
|
|
bool hasTopParentGuid = incoming.ParentGuid is not null;
|
|
bool hasTopParentLocation = incoming.ParentLocation is not null;
|
|
if (hasTopParentGuid != hasTopParentLocation)
|
|
return false;
|
|
|
|
if (incoming.Physics is not { } physics)
|
|
{
|
|
// These flattened values are parser projections of PhysicsDesc.
|
|
// Without that source block, accepting any of them would create
|
|
// contradictory admission authority before the raw create is
|
|
// either queued for a parent or assigned a residence lease.
|
|
return incoming.Position is null
|
|
&& incoming.SetupTableId is null
|
|
&& incoming.MotionState is null
|
|
&& incoming.MotionTableId is null
|
|
&& incoming.PhysicsState is null
|
|
&& incoming.ObjScale is null
|
|
&& incoming.Friction is null
|
|
&& incoming.Elasticity is null
|
|
&& incoming.InstanceSequence == 0
|
|
&& incoming.MovementSequence == 0
|
|
&& incoming.ServerControlSequence == 0
|
|
&& incoming.PositionSequence == 0
|
|
&& !hasTopParentGuid
|
|
&& incoming.PlacementId is null;
|
|
}
|
|
if (physics.Timestamps.Instance != incoming.InstanceSequence
|
|
|| physics.Timestamps.Position != incoming.PositionSequence
|
|
|| physics.Timestamps.Movement != incoming.MovementSequence
|
|
|| physics.Timestamps.ServerControlledMove
|
|
!= incoming.ServerControlSequence
|
|
|| physics.Position != incoming.Position)
|
|
return false;
|
|
|
|
PhysicsAttachment? flattenedParent = incoming.ParentGuid is { } parentGuid
|
|
&& incoming.ParentLocation is { } parentLocation
|
|
? new PhysicsAttachment(parentGuid, parentLocation)
|
|
: null;
|
|
if (flattenedParent != physics.Parent
|
|
|| incoming.PlacementId != physics.AnimationFrame)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static bool IsStructurallyValidDeferredCreate(
|
|
in WorldSession.EntitySpawn incoming)
|
|
{
|
|
if (incoming.Guid == 0u)
|
|
return false;
|
|
if (incoming.Physics is not { } physics)
|
|
return true;
|
|
if (physics.Parent is null
|
|
&& physics.Position is { LandblockId: not 0u } position
|
|
&& !RuntimeAuthoritativePositionRouteClassifier
|
|
.IsValidCreateWirePosition(position))
|
|
{
|
|
return false;
|
|
}
|
|
if (physics.Velocity is { } velocity && !IsFinite(velocity)
|
|
|| physics.Acceleration is { } acceleration
|
|
&& !IsFinite(acceleration)
|
|
|| physics.AngularVelocity is { } angularVelocity
|
|
&& !IsFinite(angularVelocity)
|
|
|| physics.Scale is { } scale && !float.IsFinite(scale)
|
|
|| physics.Friction is { } friction && !float.IsFinite(friction)
|
|
|| physics.Elasticity is { } elasticity
|
|
&& !float.IsFinite(elasticity)
|
|
|| physics.Translucency is { } translucency
|
|
&& !float.IsFinite(translucency))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private bool AdmitSameGenerationCreate(
|
|
RuntimeEntityRecord canonical,
|
|
in RuntimeInitialCreateResidenceLease prior,
|
|
in WorldSession.EntitySpawn incoming,
|
|
in InboundCreateResult admitted,
|
|
bool isLocalPlayer)
|
|
{
|
|
if (!ReferenceEquals(
|
|
canonical,
|
|
Entities.TryGetActive(
|
|
incoming.Guid,
|
|
out RuntimeEntityRecord current)
|
|
? current
|
|
: null)
|
|
|| canonical.Incarnation != incoming.InstanceSequence
|
|
|| admitted.Disposition
|
|
is not CreateObjectTimestampDisposition.ExistingGeneration
|
|
|| !InitialCreateResidences.CanEnqueue(canonical, prior))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var actions = ImmutableArray.CreateBuilder<
|
|
RuntimeInitialCreateTailAction>();
|
|
RuntimeAcceptedPositionSource positionSource =
|
|
RuntimeAcceptedPositionSource.Unknown;
|
|
|
|
if (admitted.SameGenerationEvents is { } events)
|
|
{
|
|
actions.Add(new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind
|
|
.PreTailDescriptionAdaptation,
|
|
incoming.Guid,
|
|
Description: events.Description));
|
|
|
|
if (Entities.TryAcceptDeferredObjDesc(
|
|
events.Appearance,
|
|
out _))
|
|
{
|
|
actions.Add(new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.ObjDesc,
|
|
incoming.Guid,
|
|
ObjDesc: events.Appearance));
|
|
}
|
|
|
|
if (events.Parent is { } parent)
|
|
{
|
|
if (Entities.TryAcceptDeferredCreateParent(
|
|
parent,
|
|
out _))
|
|
{
|
|
actions.Add(new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.CreateParent,
|
|
incoming.Guid,
|
|
CreateParent: parent));
|
|
}
|
|
}
|
|
else if (events.Position is { } position)
|
|
{
|
|
if (!RuntimeAuthoritativePositionRouteClassifier
|
|
.IsValidCreateWirePosition(position.Position)
|
|
|| position.Velocity is { } velocity
|
|
&& !IsFinite(velocity))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Entities.TryAcceptDeferredPosition(
|
|
position,
|
|
isLocalPlayer,
|
|
out PositionTimestampDisposition disposition,
|
|
out AcceptedPhysicsTimestamps timestamps,
|
|
out bool timestampMutation))
|
|
{
|
|
return false;
|
|
}
|
|
if (disposition is not PositionTimestampDisposition.Rejected
|
|
|| timestampMutation)
|
|
{
|
|
positionSource = RuntimeAcceptedPositionSource
|
|
.SameIncarnationCreate;
|
|
actions.Add(new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.Position,
|
|
incoming.Guid,
|
|
Position: position,
|
|
PositionSource: positionSource,
|
|
PositionDisposition: disposition,
|
|
PreviousTeleportSequence:
|
|
timestamps.PreviousTeleport,
|
|
AcceptedTimestamps: timestamps,
|
|
HasTimestampMutation: timestampMutation));
|
|
}
|
|
}
|
|
else if (events.Pickup is { } pickup
|
|
&& Entities.TryAcceptDeferredPickup(pickup, out _))
|
|
{
|
|
actions.Add(new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.Pickup,
|
|
incoming.Guid,
|
|
Pickup: pickup));
|
|
}
|
|
|
|
if (events.Movement is { } movement)
|
|
{
|
|
bool payloadApplied = Entities.TryAcceptDeferredMotion(
|
|
movement,
|
|
out AcceptedPhysicsTimestamps timestamps,
|
|
out bool timestampMutation);
|
|
if (payloadApplied || timestampMutation)
|
|
{
|
|
actions.Add(new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.Movement,
|
|
incoming.Guid,
|
|
Movement: movement,
|
|
AcceptedTimestamps: timestamps,
|
|
AppliesMovementPayload: payloadApplied,
|
|
RetainMovementPayload: true,
|
|
HasTimestampMutation: timestampMutation));
|
|
}
|
|
}
|
|
|
|
if (Entities.TryAcceptDeferredState(events.State, out _))
|
|
{
|
|
actions.Add(new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.State,
|
|
incoming.Guid,
|
|
State: events.State));
|
|
}
|
|
if (Entities.TryAcceptDeferredVector(events.Vector, out _))
|
|
{
|
|
actions.Add(new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.Vector,
|
|
incoming.Guid,
|
|
Vector: events.Vector));
|
|
}
|
|
}
|
|
|
|
actions.Add(new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.WeenieDescription,
|
|
incoming.Guid,
|
|
WeenieDescription: incoming));
|
|
actions.Add(new RuntimeInitialCreateTailAction(
|
|
RuntimeInitialCreateTailActionKind.ResidentCellCleanup,
|
|
incoming.Guid));
|
|
|
|
RuntimeInitialCreateResidenceLease retained = InitialCreateResidences
|
|
.EnqueueAccepted(
|
|
canonical,
|
|
prior,
|
|
RuntimeInitialCreateContinuationKind.SameIncarnationCreate,
|
|
positionSource,
|
|
actions.ToImmutable());
|
|
return retained.IsValid;
|
|
}
|
|
|
|
private void EnsureNotDisposed() =>
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
|
|
internal bool TryGetInitialCreateResidence(
|
|
RuntimeEntityRecord canonical,
|
|
out RuntimeInitialCreateResidenceLease lease)
|
|
{
|
|
EnsureNotDisposed();
|
|
return InitialCreateResidences.TryGetCurrent(canonical, out lease);
|
|
}
|
|
|
|
internal RuntimeInitialCreateResidenceCompletionStatus
|
|
CompleteInitialCreateResidence(
|
|
RuntimeEntityRecord canonical,
|
|
in RuntimeInitialCreateResidenceToken token,
|
|
out RuntimeInitialCreateResidenceReceipt receipt)
|
|
{
|
|
EnsureNotDisposed();
|
|
return InitialCreateResidences.Complete(
|
|
canonical,
|
|
token,
|
|
out receipt);
|
|
}
|
|
|
|
internal bool AcknowledgeInitialCreateResidenceAdoption(
|
|
RuntimeEntityRecord canonical,
|
|
in RuntimeInitialCreateResidenceAdoptionToken token)
|
|
{
|
|
EnsureNotDisposed();
|
|
return InitialCreateResidences.AcknowledgeAdoption(
|
|
canonical,
|
|
token);
|
|
}
|
|
|
|
private RuntimePlacementCancellationReceipt ForgetInitialCreateResidence(
|
|
RuntimeEntityRecord canonical)
|
|
{
|
|
bool forgotten = InitialCreateResidences.Forget(
|
|
canonical,
|
|
out _,
|
|
out RuntimePlacementCancellationReceipt cancellation);
|
|
// Round 3 B3: InitialCreateResidences.Forget's own retirement
|
|
// notification already routes to InitialCreateExecution.DiscardProgress
|
|
// for a successful Forget. This explicit call is defensive-in-depth
|
|
// for the (record never held a residence) case where Forget returns
|
|
// false without ever reaching the notification - DiscardProgress is
|
|
// idempotent, so a redundant call after a successful Forget is a
|
|
// guaranteed no-op, never a double-discard.
|
|
if (canonical.Key is { } key)
|
|
InitialCreateExecution.DiscardProgress(key);
|
|
return forgotten ? cancellation : default;
|
|
}
|
|
|
|
private static RuntimePlacementCancellationReceipt PreferCancellation(
|
|
in RuntimePlacementCancellationReceipt initial,
|
|
in RuntimePlacementCancellationReceipt ordinary) =>
|
|
initial.IsValid ? initial : ordinary;
|
|
|
|
private bool InitializeAcceptedCreateResidence(
|
|
RuntimeEntityRecord canonical,
|
|
in InboundCreateResult accepted,
|
|
bool beginInitialResidence,
|
|
bool isLocalPlayer)
|
|
{
|
|
if (!beginInitialResidence)
|
|
return true;
|
|
|
|
// The explicit cutover path separates accepted wire authority from
|
|
// committed residence before any observer can hydrate it. Failure to
|
|
// own the exact route is fail-closed; the caller removes the canonical
|
|
// record before publishing Registered/Updated.
|
|
if (canonical.PositionAuthorityVersion == 0UL)
|
|
Entities.AdvancePositionAuthority(canonical);
|
|
if (canonical.FullCellId != 0u)
|
|
Entities.SetFullCell(canonical, 0u, 0u);
|
|
RuntimeInitialCreateResidenceLease lease =
|
|
InitialCreateResidences.Begin(
|
|
canonical,
|
|
accepted,
|
|
isLocalPlayer);
|
|
return lease.IsValid;
|
|
}
|
|
|
|
private Exception FailInitialResidenceRegistration(
|
|
RuntimeEntityRecord canonical,
|
|
bool publishDeleted)
|
|
{
|
|
if (!Entities.RemoveActive(canonical))
|
|
{
|
|
return new InvalidOperationException(
|
|
$"Initial residence for 0x{canonical.ServerGuid:X8} failed after its canonical incarnation was superseded.");
|
|
}
|
|
|
|
Exception? failure = null;
|
|
if (publishDeleted)
|
|
{
|
|
try
|
|
{
|
|
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
failure = error;
|
|
}
|
|
}
|
|
failure = Combine(failure, RetireCanonicalOnly(canonical));
|
|
var cause = new InvalidOperationException(
|
|
$"Initial residence for 0x{canonical.ServerGuid:X8} could not acquire its exact Runtime placement lease.");
|
|
return failure is null
|
|
? cause
|
|
: new AggregateException(cause, failure);
|
|
}
|
|
|
|
private static Exception? Combine(
|
|
Exception? first,
|
|
Exception? second) =>
|
|
first is null
|
|
? second
|
|
: second is null
|
|
? first
|
|
: new AggregateException(first, second);
|
|
|
|
private static InboundCreateResult SupersededCreateResult() => new(
|
|
CreateObjectTimestampDisposition.StaleGeneration,
|
|
default,
|
|
null,
|
|
default);
|
|
|
|
private bool IsCurrentOperation(
|
|
uint guid,
|
|
RuntimeEntityRecord canonical,
|
|
ulong sessionVersion,
|
|
ulong operationVersion) =>
|
|
Entities.SessionLifetimeVersion == sessionVersion
|
|
&& Entities.CurrentLifetimeMutation(guid) == operationVersion
|
|
&& Entities.IsCurrent(canonical);
|
|
|
|
private RuntimeEntityRegistrationResult SupersededRegistration(
|
|
uint guid,
|
|
bool replacedExistingGeneration) =>
|
|
new(
|
|
SupersededCreateResult(),
|
|
Entities.TryGetActive(guid, out RuntimeEntityRecord current)
|
|
? current
|
|
: null,
|
|
LogicalRegistrationCreated: false,
|
|
ReplacedExistingGeneration: replacedExistingGeneration);
|
|
}
|