Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.
Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.
D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.
Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.
Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.
A1 ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
creature — a regression this slice introduced while closing a
structurally identical hole. Now maps to NearInterpolate; switch made
total with a throwing default proven unreachable.
R1 D2's write-nothing shape existed on the player arm only; NPC packets
fell through and wrote the body. Retail makes no player/NPC distinction.
R2 report_collision_end(this,1) @0x00514F31 was bound to
ShadowObjects.Suspend, a port of a DIFFERENT retail function
(remove_shadows_from_cells) that teleport_hook never calls. Now routes
to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
ForceEnd in an admission-blocking transaction so a DoCollisionEnd
callback cannot recreate the contact table.
R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
(~1,000+ m/s) and planned a run cycle from it. Both the install and
RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.
BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.
Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.
Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.
Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).
Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.
Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.
Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.
Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).
STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2700 lines
109 KiB
C#
2700 lines
109 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.Gameplay;
|
|
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,
|
|
/// <summary>
|
|
/// C3a/F2: outstanding <c>AcDream.Runtime.Gameplay.RuntimeLocalPlayerFirstEntryState</c>
|
|
/// tracked keys - dormant (no production caller of its own
|
|
/// <c>Advance</c>), but fully constructed/wired like every other owner
|
|
/// here, so its own ownership must converge to zero the same way.
|
|
/// </summary>
|
|
int LocalPlayerFirstEntryActiveCount = 0,
|
|
/// <summary>
|
|
/// C3b: outstanding <see cref="RuntimeRemoteFirstEntryState"/> tracked
|
|
/// keys - the remote/projectile Create-time body-construction conductor.
|
|
/// Dormant like its C3a sibling; converges to zero the same way.
|
|
/// </summary>
|
|
int RemoteFirstEntryActiveCount = 0,
|
|
/// <summary>
|
|
/// C3c-R1 review F5: outstanding host first-entry drive entries
|
|
/// (<c>RuntimeFirstEntryDriveController</c> pending keys, summed over
|
|
/// every drive registered against this lifetime via
|
|
/// <see cref="RuntimeEntityObjectLifetime.RegisterFirstEntryDriveOwnership"/>).
|
|
/// Previously outside every ledger; gated by <see cref="IsConverged"/>
|
|
/// like the conductor counts it pumps.
|
|
/// </summary>
|
|
int FirstEntryDrivePendingCount = 0,
|
|
/// <summary>
|
|
/// C4 route 2: outstanding host <c>RuntimeAcceptedPositionDriveController</c>
|
|
/// pending operations (a not-yet-committed or not-yet-acknowledged
|
|
/// ForcePosition on the local player), summed over every drive
|
|
/// registered against this lifetime via
|
|
/// <see cref="RuntimeEntityObjectLifetime.RegisterAcceptedPositionDriveOwnership"/>.
|
|
/// Gated by <see cref="IsConverged"/> — a leaked pending ack cannot hide.
|
|
/// </summary>
|
|
int AcceptedPositionDrivePendingCount = 0,
|
|
/// <summary>
|
|
/// C4 route 4b-1: outstanding
|
|
/// <c>AcDream.Runtime.Session.RuntimeRemotePlacementDriveController</c>
|
|
/// preparation-retry entries (a not-yet-resolved
|
|
/// <c>RetrySetupUnavailable</c>/<c>RetryWorldFrameUnavailable</c> for a
|
|
/// remote), summed over every drive registered against this lifetime via
|
|
/// <see cref="RuntimeEntityObjectLifetime.RegisterRemotePlacementDriveOwnership"/>.
|
|
/// Gated by <see cref="IsConverged"/>, mirroring
|
|
/// <see cref="AcceptedPositionDrivePendingCount"/> — steady-state
|
|
/// remotes hold no operations, and this count proves it at every
|
|
/// convergence checkpoint the same way.
|
|
/// </summary>
|
|
int RemotePlacementDrivePendingCount = 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
|
|
&& LocalPlayerFirstEntryActiveCount == 0
|
|
&& RemoteFirstEntryActiveCount == 0
|
|
&& FirstEntryDrivePendingCount == 0
|
|
&& AcceptedPositionDrivePendingCount == 0
|
|
&& RemotePlacementDrivePendingCount == 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;
|
|
/// <summary>C3c: see <see cref="BindInitialResidenceBeginNotification"/>.</summary>
|
|
private Action<RuntimeEntityRecord>? _initialResidenceBegan;
|
|
/// <summary>C3c-R1 review F5: see <see cref="RegisterFirstEntryDriveOwnership"/>.</summary>
|
|
private readonly List<Func<int>> _firstEntryDriveOwnership = [];
|
|
/// <summary>C4 route 2: see <see cref="RegisterAcceptedPositionDriveOwnership"/>.</summary>
|
|
private readonly List<Func<int>> _acceptedPositionDriveOwnership = [];
|
|
/// <summary>C4 route 4b-1: see <see cref="RegisterRemotePlacementDriveOwnership"/>.</summary>
|
|
private readonly List<Func<int>> _remotePlacementDriveOwnership = [];
|
|
/// <summary>
|
|
/// C4 route 4a: captured by <see cref="BindEventContext"/> alongside the
|
|
/// other generation-consuming children so
|
|
/// <see cref="ClassifyRemoteAcceptedPosition"/> can build a real
|
|
/// <c>RuntimeAuthoritativePositionAuthority</c> from the SAME generation
|
|
/// source every other accepted-position authority in this lifetime uses.
|
|
/// Never exposed: a host must not be able to read a generation token out
|
|
/// of this lifetime and assemble its own authority beside it.
|
|
/// </summary>
|
|
private Func<RuntimeGenerationToken>? _generation;
|
|
/// <summary>
|
|
/// #297 (review round 2, preferred fix): keeps every canonical
|
|
/// snapshot's <c>ObjectDescriptionFlags</c> live against
|
|
/// <c>ClientObjectTable.PublicWeenieBitfield</c> — see
|
|
/// <see cref="RuntimeEntityPvpBitfieldSnapshotSync"/> for the full
|
|
/// rationale.
|
|
/// </summary>
|
|
private readonly RuntimeEntityPvpBitfieldSnapshotSync _pvpBitfieldSync;
|
|
|
|
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();
|
|
_pvpBitfieldSync = new RuntimeEntityPvpBitfieldSnapshotSync(Entities, Objects);
|
|
// 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));
|
|
// C3a/F2: dormant - no production caller of its own Advance - but
|
|
// constructed and wired exactly like the executor above so its
|
|
// ownership converges the same way. RuntimeLocalPlayerPhysicsPublicationState
|
|
// does not exist yet at this point (GameRuntime builds
|
|
// RuntimeLocalPlayerMovementState, then attaches its publication,
|
|
// only after this lifetime); BindPublication supplies it later.
|
|
LocalPlayerFirstEntry = new RuntimeLocalPlayerFirstEntryState(
|
|
InitialCreateResidences,
|
|
InitialCreateExecution,
|
|
Physics);
|
|
// C3b: the remote/projectile analog of the conductor above — retail
|
|
// body construction at Create time in place of the publication
|
|
// chain. Dormant like its sibling (no production caller of Advance);
|
|
// constructed and wired identically so its ownership converges the
|
|
// same way.
|
|
RemoteFirstEntry = new RuntimeRemoteFirstEntryState(
|
|
InitialCreateResidences,
|
|
InitialCreateExecution,
|
|
Physics);
|
|
// 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));
|
|
// C3a/F2: the SAME multicast retirement notification also reaps the
|
|
// first-entry conductor's tracked progress - see
|
|
// RuntimeInitialCreateResidenceState.BindRetirementNotification's
|
|
// updated doc comment for why this is now multicast.
|
|
InitialCreateResidences.BindRetirementNotification(
|
|
key => LocalPlayerFirstEntry.Forget(key));
|
|
// C3b: the remote conductor joins the SAME multicast retirement
|
|
// fan-out, third in registration order.
|
|
InitialCreateResidences.BindRetirementNotification(
|
|
key => RemoteFirstEntry.Forget(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,
|
|
InitialCreateExecution);
|
|
}
|
|
|
|
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();
|
|
_pvpBitfieldSync = new RuntimeEntityPvpBitfieldSnapshotSync(Entities, 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));
|
|
// C3a/F2: dormant - no production caller of its own Advance - but
|
|
// constructed and wired exactly like the executor above so its
|
|
// ownership converges the same way. RuntimeLocalPlayerPhysicsPublicationState
|
|
// does not exist yet at this point (GameRuntime builds
|
|
// RuntimeLocalPlayerMovementState, then attaches its publication,
|
|
// only after this lifetime); BindPublication supplies it later.
|
|
LocalPlayerFirstEntry = new RuntimeLocalPlayerFirstEntryState(
|
|
InitialCreateResidences,
|
|
InitialCreateExecution,
|
|
Physics);
|
|
// C3b: the remote/projectile analog of the conductor above — retail
|
|
// body construction at Create time in place of the publication
|
|
// chain. Dormant like its sibling (no production caller of Advance);
|
|
// constructed and wired identically so its ownership converges the
|
|
// same way.
|
|
RemoteFirstEntry = new RuntimeRemoteFirstEntryState(
|
|
InitialCreateResidences,
|
|
InitialCreateExecution,
|
|
Physics);
|
|
// 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));
|
|
// C3a/F2: the SAME multicast retirement notification also reaps the
|
|
// first-entry conductor's tracked progress - see
|
|
// RuntimeInitialCreateResidenceState.BindRetirementNotification's
|
|
// updated doc comment for why this is now multicast.
|
|
InitialCreateResidences.BindRetirementNotification(
|
|
key => LocalPlayerFirstEntry.Forget(key));
|
|
// C3b: the remote conductor joins the SAME multicast retirement
|
|
// fan-out, third in registration order.
|
|
InitialCreateResidences.BindRetirementNotification(
|
|
key => RemoteFirstEntry.Forget(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,
|
|
InitialCreateExecution);
|
|
}
|
|
|
|
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();
|
|
_pvpBitfieldSync = new RuntimeEntityPvpBitfieldSnapshotSync(Entities, 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));
|
|
// C3a/F2: dormant - no production caller of its own Advance - but
|
|
// constructed and wired exactly like the executor above so its
|
|
// ownership converges the same way. RuntimeLocalPlayerPhysicsPublicationState
|
|
// does not exist yet at this point (GameRuntime builds
|
|
// RuntimeLocalPlayerMovementState, then attaches its publication,
|
|
// only after this lifetime); BindPublication supplies it later.
|
|
LocalPlayerFirstEntry = new RuntimeLocalPlayerFirstEntryState(
|
|
InitialCreateResidences,
|
|
InitialCreateExecution,
|
|
Physics);
|
|
// C3b: the remote/projectile analog of the conductor above — retail
|
|
// body construction at Create time in place of the publication
|
|
// chain. Dormant like its sibling (no production caller of Advance);
|
|
// constructed and wired identically so its ownership converges the
|
|
// same way.
|
|
RemoteFirstEntry = new RuntimeRemoteFirstEntryState(
|
|
InitialCreateResidences,
|
|
InitialCreateExecution,
|
|
Physics);
|
|
// 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));
|
|
// C3a/F2: the SAME multicast retirement notification also reaps the
|
|
// first-entry conductor's tracked progress - see
|
|
// RuntimeInitialCreateResidenceState.BindRetirementNotification's
|
|
// updated doc comment for why this is now multicast.
|
|
InitialCreateResidences.BindRetirementNotification(
|
|
key => LocalPlayerFirstEntry.Forget(key));
|
|
// C3b: the remote conductor joins the SAME multicast retirement
|
|
// fan-out, third in registration order.
|
|
InitialCreateResidences.BindRetirementNotification(
|
|
key => RemoteFirstEntry.Forget(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,
|
|
InitialCreateExecution);
|
|
}
|
|
|
|
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; }
|
|
internal RuntimeLocalPlayerFirstEntryState LocalPlayerFirstEntry { get; }
|
|
internal RuntimeRemoteFirstEntryState RemoteFirstEntry { 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,
|
|
LocalPlayerFirstEntry.CaptureOwnership().ActiveCount,
|
|
RemoteFirstEntry.CaptureOwnership().ActiveCount,
|
|
CaptureFirstEntryDrivePendingCount(),
|
|
CaptureAcceptedPositionDrivePendingCount(),
|
|
CaptureRemotePlacementDrivePendingCount());
|
|
}
|
|
|
|
private int CaptureFirstEntryDrivePendingCount()
|
|
{
|
|
int total = 0;
|
|
for (int i = 0; i < _firstEntryDriveOwnership.Count; i++)
|
|
total = checked(total + _firstEntryDriveOwnership[i]());
|
|
return total;
|
|
}
|
|
|
|
private int CaptureAcceptedPositionDrivePendingCount()
|
|
{
|
|
int total = 0;
|
|
for (int i = 0; i < _acceptedPositionDriveOwnership.Count; i++)
|
|
total = checked(total + _acceptedPositionDriveOwnership[i]());
|
|
return total;
|
|
}
|
|
|
|
private int CaptureRemotePlacementDrivePendingCount()
|
|
{
|
|
int total = 0;
|
|
for (int i = 0; i < _remotePlacementDriveOwnership.Count; i++)
|
|
total = checked(total + _remotePlacementDriveOwnership[i]());
|
|
return total;
|
|
}
|
|
|
|
/// <summary>
|
|
/// C3c-R1 review F5: registers one host first-entry drive controller's
|
|
/// pending-count provider into this lifetime's ownership snapshot, so
|
|
/// tracked-but-undriven entries can never sit outside every ledger. The
|
|
/// drive controller registers itself at construction (it already binds
|
|
/// <see cref="BindInitialResidenceBeginNotification"/> there); multiple
|
|
/// registrations sum, mirroring the multicast notification shape.
|
|
/// </summary>
|
|
public void RegisterFirstEntryDriveOwnership(Func<int> pendingCount)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(pendingCount);
|
|
EnsureNotDisposed();
|
|
_firstEntryDriveOwnership.Add(pendingCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C4 route 2: registers one host <c>RuntimeAcceptedPositionDriveController</c>'s
|
|
/// pending-count provider into this lifetime's ownership snapshot,
|
|
/// mirroring <see cref="RegisterFirstEntryDriveOwnership"/> — a leaked
|
|
/// pending ForcePosition ack must not sit outside every ledger. The
|
|
/// drive controller registers itself at construction; multiple
|
|
/// registrations sum (one per host route sharing this lifetime).
|
|
/// </summary>
|
|
public void RegisterAcceptedPositionDriveOwnership(Func<int> pendingCount)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(pendingCount);
|
|
EnsureNotDisposed();
|
|
_acceptedPositionDriveOwnership.Add(pendingCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C4 route 4b-1: registers one host
|
|
/// <c>RuntimeRemotePlacementDriveController</c>'s pending-count provider
|
|
/// into this lifetime's ownership snapshot, mirroring
|
|
/// <see cref="RegisterAcceptedPositionDriveOwnership"/> — a leaked
|
|
/// remote preparation retry must not sit outside every ledger. The drive
|
|
/// controller registers itself at construction; multiple registrations
|
|
/// sum (one per host route sharing this lifetime).
|
|
/// </summary>
|
|
public void RegisterRemotePlacementDriveOwnership(Func<int> pendingCount)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(pendingCount);
|
|
EnsureNotDisposed();
|
|
_remotePlacementDriveOwnership.Add(pendingCount);
|
|
}
|
|
|
|
public void BindEventContext(
|
|
Func<RuntimeGenerationToken> generation,
|
|
Func<ulong> frameNumber)
|
|
{
|
|
EnsureNotDisposed();
|
|
_generation = generation;
|
|
Events.BindContext(generation, frameNumber);
|
|
Placements.BindGeneration(generation);
|
|
InitialCreateResidences.BindGeneration(generation);
|
|
InitialCreateExecution.BindGeneration(generation);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C4 route 4a: classifies one REMOTE incarnation's accepted Position
|
|
/// through <see cref="RuntimeAuthoritativePositionRouteClassifier"/>, so
|
|
/// the graphical host and any future no-window remote-motion host make
|
|
/// the SAME airborne-no-op / near-interpolate decision from the same
|
|
/// generation, the same authority shape, and the same request builder the
|
|
/// deferred initial-create continuation uses.
|
|
///
|
|
/// <para>
|
|
/// Returns <see langword="null"/> when no classification can honestly be
|
|
/// made: the lifetime has no bound generation yet, the canonical record
|
|
/// has not claimed a local id, or there is no live local-player position
|
|
/// to derive retail's <c>player_distance</c> from. A null here is
|
|
/// "Runtime has no opinion", never "rejected".
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// C4 route 4b-2 review fix — this used to add "In every one of those
|
|
/// cases the caller's pre-existing legacy path runs completely
|
|
/// unchanged". That path no longer exists: 4b-2 deleted the graphical
|
|
/// caller's duplicated near/far blocks, and a null now takes the stated
|
|
/// <c>UnroutedCatchUp</c> policy (AP-137;
|
|
/// <c>RuntimeRemoteFarSnapPosition.ResolveArm</c>), which is AP-87's
|
|
/// shared catch-up rather than a re-derived 96 m test.
|
|
/// </para>
|
|
/// </summary>
|
|
internal RuntimeAuthoritativePositionRoute? ClassifyRemoteAcceptedPosition(
|
|
RuntimeEntityRecord canonical,
|
|
in WorldSession.EntityPositionUpdate update,
|
|
PositionTimestampDisposition disposition,
|
|
in AcceptedPhysicsTimestamps timestamps,
|
|
float? playerDistance)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(canonical);
|
|
if (_generation is not { } generation
|
|
// C4 route 4b-3 (D1): a remote PositionEvent feeds the
|
|
// classifier's cell-less predicate the PRE-merge committed cell
|
|
// — the same value TryApplyPosition measured for THIS packet,
|
|
// never re-read from the (already merged) canonical record. A
|
|
// null here means the merge never observed a prior canonical
|
|
// record for this entity, which "Runtime has no opinion" — the
|
|
// same policy every other missing-input case in this method
|
|
// already uses — covers honestly rather than fabricating 0.
|
|
|| timestamps.PreMergeCommittedCellId is not { } preMergeCommittedCellId)
|
|
{
|
|
return null;
|
|
}
|
|
if (!RuntimeAcceptedPositionRouteRequests.TryBuild(
|
|
generation(),
|
|
canonical,
|
|
update,
|
|
RuntimePositionEntityKind.Remote,
|
|
RuntimeAcceptedPositionSource.PositionEvent,
|
|
disposition,
|
|
timestamps.PreviousTeleport,
|
|
timestamps.Teleport,
|
|
playerDistance,
|
|
// Retail's UsePositionFromServer is consumed by the local
|
|
// player branch only; the Remote branch never reads it.
|
|
usePositionFromServer: false,
|
|
preMergeCommittedCellId,
|
|
out RuntimeAcceptedPositionRouteRequest request))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return RuntimeAuthoritativePositionRouteClassifier
|
|
.ClassifyAcceptedPosition(request);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C3c: registers one host callback fired for every FRESH initial-create
|
|
/// residence begin (never for a same-generation FIFO append). Multicast,
|
|
/// mirroring <see cref="RuntimeInitialCreateResidenceState.BindRetirementNotification"/>.
|
|
/// The callback runs synchronously inside the registration transaction —
|
|
/// subscribers must only record the entity for a later drive pump, never
|
|
/// call a conductor's Advance re-entrantly from it.
|
|
/// </summary>
|
|
public void BindInitialResidenceBeginNotification(
|
|
Action<RuntimeEntityRecord> began)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(began);
|
|
EnsureNotDisposed();
|
|
_initialResidenceBegan += began;
|
|
}
|
|
|
|
/// <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 (beginInitialResidence && isLocalPlayer)
|
|
{
|
|
// The accepted local Create establishes the shared world frame
|
|
// before any remote first-entry conductor converts its authored
|
|
// landblock-local coordinates. #284: observe the Create itself
|
|
// even when it carries no usable landblock - that is precisely
|
|
// the case in which no frame is ever published, and every remote
|
|
// placement would otherwise park forever without a diagnostic.
|
|
Physics.ObserveLocalPlayerCreate(
|
|
(incoming.Physics?.Position ?? incoming.Position)
|
|
?.LandblockId ?? 0u);
|
|
}
|
|
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,
|
|
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 (isLocalPlayer
|
|
&& disposition is not PositionTimestampDisposition.Rejected)
|
|
{
|
|
Physics.ObserveLocalWorldFrame(
|
|
update.Position.LandblockId,
|
|
timestamps.TeleportAdvanced);
|
|
}
|
|
|
|
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 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;
|
|
}
|
|
|
|
if (isLocalPlayer
|
|
&& disposition is not PositionTimestampDisposition.Rejected)
|
|
{
|
|
Physics.ObserveLocalWorldFrame(
|
|
update.Position.LandblockId,
|
|
timestamps.TeleportAdvanced);
|
|
}
|
|
|
|
bool acceptedPosition =
|
|
disposition is not PositionTimestampDisposition.Rejected;
|
|
// C4 route 4b-3 (D1): the classifier's remote cell-less predicate
|
|
// needs the PRE-merge committed cell — the value this method just
|
|
// measured as `beforeCell`, before `RefreshSnapshot`
|
|
// below stamps the accepted wire cell onto the canonical record via
|
|
// `RefreshDerivedState` -> `SetFullCell`. Reading `canonical.FullCellId`
|
|
// AFTER that merge (as the classifier's default builder overload
|
|
// does for every other caller) always sees the wire cell, which is
|
|
// why the predicate as fed to a remote PositionEvent was dead before
|
|
// this fix. `hadCanonical` is what makes this an honest value rather
|
|
// than a fabricated 0 — see the field's own doc.
|
|
timestamps = timestamps with
|
|
{
|
|
PreMergeCommittedCellId = hadCanonical ? beforeCell : null,
|
|
};
|
|
RuntimePlacementCancellationReceipt cancellation = default;
|
|
if (acceptedPosition)
|
|
{
|
|
// A CANCELLATION, not a withdrawal: a newer accepted Position
|
|
// supersedes the in-flight placement but the entity stays in the
|
|
// world. If that placement was a DeferredCell park, cancelling it
|
|
// without rolling the withdrawal back left the entity invisible
|
|
// AND intangible with nothing able to wake it - see
|
|
// RuntimeSetPositionState.Forget. Every OTHER Forget call in this
|
|
// class is a withdrawal transaction and deliberately does not opt
|
|
// in.
|
|
cancellation = Physics.SetPosition.Forget(
|
|
canonical,
|
|
restoreCancelledPark: true);
|
|
}
|
|
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();
|
|
LocalPlayerFirstEntry.DiscardAll();
|
|
RemoteFirstEntry.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;
|
|
_generation = null;
|
|
_pvpBitfieldSync.Dispose();
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C3c-R1 review F7: host seam for a bounded-collision-neighborhood
|
|
/// host to convert a remote/projectile Create's active residence to the
|
|
/// celless completion route when its destination landblock will never
|
|
/// be collision-published (a headless far remote). See
|
|
/// <see cref="RuntimeInitialCreateResidenceState.TryConvertToCellessRoute"/>.
|
|
/// </summary>
|
|
public bool TryConvertInitialResidenceToCellessRoute(
|
|
RuntimeEntityRecord canonical)
|
|
{
|
|
EnsureNotDisposed();
|
|
return InitialCreateResidences.TryConvertToCellessRoute(canonical);
|
|
}
|
|
|
|
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);
|
|
if (!lease.IsValid)
|
|
return false;
|
|
// C3c: host drive notification. Fires for EVERY fresh residence
|
|
// begin through this single choke point — wire-dispatch Creates AND
|
|
// the executor's deferred-child replays (which register through this
|
|
// class's own bound delegate, never through a host runtime). The
|
|
// subscriber must only RECORD the key for a later drive pump — this
|
|
// fires mid-registration, before Registered publishes, and a
|
|
// synchronous Advance here would interleave with the enclosing
|
|
// transaction (and, for a replayed child, with the parent's own
|
|
// in-flight Execute).
|
|
_initialResidenceBegan?.Invoke(canonical);
|
|
return true;
|
|
}
|
|
|
|
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);
|
|
}
|