diff --git a/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs b/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs
index a88a9058..8f7674f0 100644
--- a/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs
+++ b/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs
@@ -20,13 +20,35 @@ public sealed class InboundPhysicsStateController
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
_snapshots.TryGetValue(guid, out spawn);
+ internal bool TryGetAcceptedTimestamps(
+ uint guid,
+ out AcceptedPhysicsTimestamps timestamps)
+ {
+ if (_gates.TryGetValue(guid, out PhysicsTimestampGate? gate))
+ {
+ timestamps = Current(gate);
+ return true;
+ }
+ timestamps = default;
+ return false;
+ }
+
public CreateObjectTimestampDisposition PreviewCreateDisposition(
WorldSession.EntitySpawn incoming) =>
_gates.TryGetValue(incoming.Guid, out PhysicsTimestampGate? gate)
? gate.PreviewCreateObject(incoming.InstanceSequence)
: CreateObjectTimestampDisposition.InitialGeneration;
- public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming)
+ public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming) =>
+ AcceptCreateCore(incoming, deferSameGenerationWeenieDescription: false);
+
+ internal InboundCreateResult AcceptCreateDeferredSameGeneration(
+ WorldSession.EntitySpawn incoming) =>
+ AcceptCreateCore(incoming, deferSameGenerationWeenieDescription: true);
+
+ private InboundCreateResult AcceptCreateCore(
+ WorldSession.EntitySpawn incoming,
+ bool deferSameGenerationWeenieDescription)
{
if (!_gates.TryGetValue(incoming.Guid, out PhysicsTimestampGate? gate))
{
@@ -56,8 +78,11 @@ public sealed class InboundPhysicsStateController
return new InboundCreateResult(disposition, incoming, null, Current(gate));
}
- WorldSession.EntitySpawn merged = MergeUntimestampedCreate(retained, incoming);
- _snapshots[incoming.Guid] = merged;
+ WorldSession.EntitySpawn merged = deferSameGenerationWeenieDescription
+ ? retained
+ : MergeUntimestampedCreate(retained, incoming);
+ if (!deferSameGenerationWeenieDescription)
+ _snapshots[incoming.Guid] = merged;
return new InboundCreateResult(
disposition,
merged,
@@ -422,6 +447,181 @@ public sealed class InboundPhysicsStateController
return true;
}
+ ///
+ /// Consumes only the retail Position timestamp gates for a deferred
+ /// initial-Create continuation. The raw PositionPack is not transformed
+ /// and no pose, parent, placement, or velocity field is installed. The
+ /// returned authority is sufficient for one later execution without
+ /// running the gate a second time.
+ ///
+ internal bool TryAcceptDeferredPosition(
+ WorldSession.EntityPositionUpdate update,
+ bool isLocalPlayer,
+ out PositionTimestampDisposition disposition,
+ out AcceptedPhysicsTimestamps timestamps,
+ out bool hasTimestampMutation)
+ {
+ if (!TryGet(
+ update.Guid,
+ out PhysicsTimestampGate? gate,
+ out WorldSession.EntitySpawn old))
+ {
+ disposition = PositionTimestampDisposition.Rejected;
+ timestamps = default;
+ hasTimestampMutation = false;
+ return false;
+ }
+
+ ushort previousPosition = gate.PositionTimestamp;
+ ushort previousTeleport = gate.TeleportTimestamp;
+ ushort previousForcePosition = gate.ForcePositionTimestamp;
+ bool advancesTeleport = PhysicsTimestampGate.IsNewer(
+ previousTeleport,
+ update.TeleportSequence);
+ disposition = gate.TryAcceptPositionEvent(
+ update.InstanceSequence,
+ update.PositionSequence,
+ update.TeleportSequence,
+ update.ForcePositionSequence,
+ isLocalPlayer);
+ timestamps = Current(
+ gate,
+ teleportAdvanced: disposition is PositionTimestampDisposition.Apply
+ && advancesTeleport,
+ previousTeleport: previousTeleport);
+ hasTimestampMutation = previousPosition != gate.PositionTimestamp
+ || previousTeleport != gate.TeleportTimestamp
+ || previousForcePosition != gate.ForcePositionTimestamp;
+ return true;
+ }
+
+ internal bool TryAcceptDeferredObjDesc(
+ ObjDescEvent.Parsed update,
+ out AcceptedPhysicsTimestamps timestamps)
+ {
+ if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out _)
+ || !gate.TryAcceptObjDescEvent(
+ update.InstanceSequence,
+ update.ObjDescSequence))
+ {
+ timestamps = default;
+ return false;
+ }
+ timestamps = Current(gate);
+ return true;
+ }
+
+ internal bool TryAcceptDeferredPickup(
+ PickupEvent.Parsed update,
+ out AcceptedPhysicsTimestamps timestamps)
+ {
+ if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out _)
+ || !gate.TryAcceptPositionChannelEvent(
+ update.InstanceSequence,
+ update.PositionSequence))
+ {
+ timestamps = default;
+ return false;
+ }
+ timestamps = Current(gate);
+ return true;
+ }
+
+ internal bool TryAcceptDeferredCreateParent(
+ CreateParentUpdate update,
+ out AcceptedPhysicsTimestamps timestamps)
+ {
+ if (!TryGet(update.ChildGuid, out PhysicsTimestampGate? gate, out _)
+ || !gate.TryAcceptPositionChannelEvent(
+ update.ChildInstanceSequence,
+ update.ChildPositionSequence))
+ {
+ timestamps = default;
+ return false;
+ }
+ timestamps = Current(gate);
+ return true;
+ }
+
+ internal bool TryAcceptDeferredParent(
+ ParentEvent.Parsed update,
+ out AcceptedPhysicsTimestamps timestamps)
+ {
+ if (!_gates.TryGetValue(
+ update.ParentGuid,
+ out PhysicsTimestampGate? parentGate)
+ || !parentGate.IsCurrentInstance(update.ParentInstanceSequence)
+ || !TryGet(
+ update.ChildGuid,
+ out PhysicsTimestampGate? childGate,
+ out _)
+ || !childGate.TryAcceptPositionChannelEvent(
+ childGate.InstanceTimestamp,
+ update.ChildPositionSequence))
+ {
+ timestamps = default;
+ return false;
+ }
+ timestamps = Current(childGate);
+ return true;
+ }
+
+ internal bool TryAcceptDeferredMotion(
+ WorldSession.EntityMotionUpdate update,
+ out AcceptedPhysicsTimestamps timestamps,
+ out bool hasTimestampMutation)
+ {
+ if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out _))
+ {
+ timestamps = default;
+ hasTimestampMutation = false;
+ return false;
+ }
+
+ ushort previousMovement = gate.MovementTimestamp;
+ ushort previousServerControl = gate.ServerControlledMoveTimestamp;
+ bool accepted = gate.TryAcceptMovementEvent(
+ update.InstanceSequence,
+ update.MovementSequence,
+ update.ServerControlSequence);
+ timestamps = Current(gate);
+ hasTimestampMutation = previousMovement != gate.MovementTimestamp
+ || previousServerControl != gate.ServerControlledMoveTimestamp;
+ return accepted;
+ }
+
+ internal bool TryAcceptDeferredState(
+ SetState.Parsed update,
+ out AcceptedPhysicsTimestamps timestamps)
+ {
+ if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out _)
+ || !gate.TryAcceptStateEvent(
+ update.InstanceSequence,
+ update.StateSequence))
+ {
+ timestamps = default;
+ return false;
+ }
+ timestamps = Current(gate);
+ return true;
+ }
+
+ internal bool TryAcceptDeferredVector(
+ VectorUpdate.Parsed update,
+ out AcceptedPhysicsTimestamps timestamps)
+ {
+ if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out _)
+ || !gate.TryAcceptVectorEvent(
+ update.InstanceSequence,
+ update.VectorSequence))
+ {
+ timestamps = default;
+ return false;
+ }
+ timestamps = Current(gate);
+ return true;
+ }
+
///
/// F751 is a notification gate only. Retail compares it to TELEPORT_TS but
/// advances that timestamp later, with the accepted Position packet.
@@ -465,12 +665,15 @@ public sealed class InboundPhysicsStateController
private static AcceptedPhysicsTimestamps Current(
PhysicsTimestampGate gate,
- bool teleportAdvanced = false) => new(
+ bool teleportAdvanced = false,
+ ushort previousTeleport = 0) => new(
gate.InstanceTimestamp,
gate.ServerControlledMoveTimestamp,
gate.TeleportTimestamp,
gate.ForcePositionTimestamp,
- teleportAdvanced);
+ teleportAdvanced,
+ TeleportHookRequired: false,
+ previousTeleport);
private static WorldSession.EntitySpawn MirrorGateTimestamps(
WorldSession.EntitySpawn spawn,
@@ -671,7 +874,8 @@ public readonly record struct AcceptedPhysicsTimestamps(
ushort Teleport,
ushort ForcePosition,
bool TeleportAdvanced = false,
- bool TeleportHookRequired = false);
+ bool TeleportHookRequired = false,
+ ushort PreviousTeleport = 0);
public readonly record struct CreateParentUpdate(
uint ChildGuid,
diff --git a/src/AcDream.Runtime/Entities/ParentAttachmentState.cs b/src/AcDream.Runtime/Entities/ParentAttachmentState.cs
index 355d139e..5405ecc7 100644
--- a/src/AcDream.Runtime/Entities/ParentAttachmentState.cs
+++ b/src/AcDream.Runtime/Entities/ParentAttachmentState.cs
@@ -1,3 +1,4 @@
+using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
@@ -17,12 +18,119 @@ public sealed class ParentAttachmentState
private readonly Dictionary _recoveryByChild = new();
private readonly Dictionary _lastAcceptedByChild = new();
private readonly Dictionary> _committedChildrenByParent = new();
+ private readonly Dictionary>
+ _deferredCreatesByParent = [];
+ private ulong _nextDeferredCreateAdmissionId;
public int UnresolvedRelationCount =>
_unresolvedByChild.Values.Sum(queue => queue.Count);
public int StagedRelationCount => _stagedByChild.Count;
public int RecoveryRelationCount => _recoveryByChild.Count;
public int CommittedRelationCount => _lastAcceptedByChild.Count;
+ internal int DeferredCreateCount =>
+ _deferredCreatesByParent.Values.Sum(queue => queue.Count);
+
+ ///
+ /// Retains the complete unaccepted CreateObject packet when its nonzero
+ /// parent is not addressable. Retail queues the raw blob before object
+ /// lookup and before any child timestamp is consumed; storing a decoded
+ /// relation after AcceptCreate would partially admit the child.
+ ///
+ internal void EnqueueDeferredCreate(
+ WorldSession.EntitySpawn spawn,
+ bool isLocalPlayer)
+ {
+ uint parentGuid = spawn.ParentGuid
+ ?? spawn.Physics?.Parent?.Guid
+ ?? 0u;
+ if (spawn.Guid == 0u || parentGuid == 0u)
+ {
+ throw new ArgumentException(
+ "A deferred parent CreateObject requires nonzero child and parent GUIDs.",
+ nameof(spawn));
+ }
+ if (_nextDeferredCreateAdmissionId == ulong.MaxValue)
+ {
+ throw new InvalidOperationException(
+ "The deferred parent CreateObject admission sequence is exhausted.");
+ }
+ if (!_deferredCreatesByParent.TryGetValue(
+ parentGuid,
+ out Queue? queue))
+ {
+ queue = new Queue();
+ _deferredCreatesByParent.Add(parentGuid, queue);
+ }
+ ulong admissionId = _nextDeferredCreateAdmissionId + 1UL;
+ queue.Enqueue(new DeferredParentCreate(
+ admissionId,
+ RuntimeInitialCreateAdmissionFreezer.Freeze(spawn),
+ isLocalPlayer));
+ _nextDeferredCreateAdmissionId = admissionId;
+ }
+
+ internal bool TryPeekDeferredCreate(
+ uint parentGuid,
+ out DeferredParentCreate deferred)
+ {
+ if (!_deferredCreatesByParent.TryGetValue(
+ parentGuid,
+ out Queue? queue)
+ || !queue.TryPeek(out deferred))
+ {
+ deferred = default;
+ return false;
+ }
+ return true;
+ }
+
+ internal bool ConsumeDeferredCreate(
+ uint parentGuid,
+ in DeferredParentCreate expected)
+ {
+ if (!_deferredCreatesByParent.TryGetValue(
+ parentGuid,
+ out Queue? queue)
+ || !queue.TryPeek(out DeferredParentCreate current)
+ || current != expected)
+ {
+ return false;
+ }
+ _ = queue.Dequeue();
+ if (queue.Count == 0)
+ _deferredCreatesByParent.Remove(parentGuid);
+ return true;
+ }
+
+ internal bool ContainsDeferredCreate(
+ uint childGuid,
+ ushort instanceSequence)
+ {
+ foreach (Queue queue
+ in _deferredCreatesByParent.Values)
+ {
+ if (queue.Any(candidate =>
+ candidate.Spawn.Guid == childGuid
+ && candidate.Spawn.InstanceSequence == instanceSequence))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Cancels only the raw, still-unaccepted child generation addressed by a
+ /// terminal packet. Instance zero is a normal retail timestamp and is not
+ /// treated as an empty sentinel.
+ ///
+ internal void CancelDeferredChildGeneration(
+ uint childGuid,
+ ushort terminalInstanceSequence) => FilterDeferredCreates(
+ candidate => candidate.Spawn.Guid != childGuid
+ || PhysicsTimestampGate.IsNewer(
+ terminalInstanceSequence,
+ candidate.Spawn.InstanceSequence));
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
{
@@ -270,6 +378,7 @@ public sealed class ParentAttachmentState
public void RemoveObject(uint guid)
{
+ RemoveDeferredChildCreates(guid);
_stagedByChild.Remove(guid);
_recoveryByChild.Remove(guid);
RemoveCommittedChild(guid);
@@ -298,6 +407,12 @@ public sealed class ParentAttachmentState
///
public void EndGeneration(uint guid, ushort replacementGeneration)
{
+ FilterDeferredCreates(candidate =>
+ candidate.Spawn.Guid != guid
+ || candidate.Spawn.InstanceSequence == replacementGeneration
+ || PhysicsTimestampGate.IsNewer(
+ replacementGeneration,
+ candidate.Spawn.InstanceSequence));
FilterChildCandidates(
guid,
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
@@ -322,6 +437,7 @@ public sealed class ParentAttachmentState
///
public void DeleteGeneration(uint guid, ushort deletedGeneration)
{
+ CancelDeferredChildGeneration(guid, deletedGeneration);
FilterChildCandidates(
guid,
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
@@ -351,6 +467,7 @@ public sealed class ParentAttachmentState
public void RemoveChild(uint childGuid)
{
+ RemoveDeferredChildCreates(childGuid);
_stagedByChild.Remove(childGuid);
_recoveryByChild.Remove(childGuid);
RemoveCommittedChild(childGuid);
@@ -359,6 +476,7 @@ public sealed class ParentAttachmentState
public void Clear()
{
+ _deferredCreatesByParent.Clear();
_unresolvedByChild.Clear();
_stagedByChild.Clear();
_recoveryByChild.Clear();
@@ -368,6 +486,26 @@ public sealed class ParentAttachmentState
_committedChildrenByParent.Clear();
}
+ private void RemoveDeferredChildCreates(uint childGuid)
+ => FilterDeferredCreates(
+ candidate => candidate.Spawn.Guid != childGuid);
+
+ private void FilterDeferredCreates(
+ Func retain)
+ {
+ uint[] parents = _deferredCreatesByParent.Keys.ToArray();
+ for (int index = 0; index < parents.Length; index++)
+ {
+ uint parentGuid = parents[index];
+ Queue retained = new(
+ _deferredCreatesByParent[parentGuid].Where(retain));
+ if (retained.Count == 0)
+ _deferredCreatesByParent.Remove(parentGuid);
+ else
+ _deferredCreatesByParent[parentGuid] = retained;
+ }
+ }
+
private void RemoveCommittedChild(uint childGuid)
{
if (!_lastAcceptedByChild.Remove(
@@ -462,6 +600,15 @@ public sealed class ParentAttachmentState
ushort InstanceSequence);
}
+internal readonly record struct DeferredParentCreate(
+ ulong AdmissionId,
+ WorldSession.EntitySpawn Spawn,
+ bool IsLocalPlayer)
+{
+ internal bool IsValid => AdmissionId != 0UL
+ && Spawn.Guid != 0u;
+}
+
public readonly record struct ParentAttachmentRelation(
uint ParentGuid,
uint ChildGuid,
diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs b/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs
index 293f34db..f037dbf5 100644
--- a/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs
+++ b/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs
@@ -1,4 +1,5 @@
using AcDream.Core.Net;
+using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Physics;
@@ -47,6 +48,10 @@ public sealed class RuntimeEntityDirectory
public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming) =>
_inbound.AcceptCreate(incoming);
+ internal InboundCreateResult AcceptCreateDeferredSameGeneration(
+ WorldSession.EntitySpawn incoming) =>
+ _inbound.AcceptCreateDeferredSameGeneration(incoming);
+
public CreateObjectTimestampDisposition PreviewCreateDisposition(
WorldSession.EntitySpawn incoming) =>
_inbound.PreviewCreateDisposition(incoming);
@@ -59,6 +64,11 @@ public sealed class RuntimeEntityDirectory
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
_inbound.TryGetSnapshot(guid, out spawn);
+ internal bool TryGetAcceptedTimestamps(
+ uint guid,
+ out AcceptedPhysicsTimestamps timestamps) =>
+ _inbound.TryGetAcceptedTimestamps(guid, out timestamps);
+
public bool TryGetActive(uint guid, out RuntimeEntityRecord record) =>
_activeByGuid.TryGetValue(guid, out record!);
@@ -482,6 +492,58 @@ public sealed class RuntimeEntityDirectory
out accepted,
out timestamps);
+ internal bool TryAcceptDeferredPosition(
+ WorldSession.EntityPositionUpdate update,
+ bool isLocalPlayer,
+ out PositionTimestampDisposition disposition,
+ out AcceptedPhysicsTimestamps timestamps,
+ out bool hasTimestampMutation) =>
+ _inbound.TryAcceptDeferredPosition(
+ update,
+ isLocalPlayer,
+ out disposition,
+ out timestamps,
+ out hasTimestampMutation);
+
+ internal bool TryAcceptDeferredObjDesc(
+ ObjDescEvent.Parsed update,
+ out AcceptedPhysicsTimestamps timestamps) =>
+ _inbound.TryAcceptDeferredObjDesc(update, out timestamps);
+
+ internal bool TryAcceptDeferredPickup(
+ PickupEvent.Parsed update,
+ out AcceptedPhysicsTimestamps timestamps) =>
+ _inbound.TryAcceptDeferredPickup(update, out timestamps);
+
+ internal bool TryAcceptDeferredCreateParent(
+ CreateParentUpdate update,
+ out AcceptedPhysicsTimestamps timestamps) =>
+ _inbound.TryAcceptDeferredCreateParent(update, out timestamps);
+
+ internal bool TryAcceptDeferredParent(
+ ParentEvent.Parsed update,
+ out AcceptedPhysicsTimestamps timestamps) =>
+ _inbound.TryAcceptDeferredParent(update, out timestamps);
+
+ internal bool TryAcceptDeferredMotion(
+ WorldSession.EntityMotionUpdate update,
+ out AcceptedPhysicsTimestamps timestamps,
+ out bool hasTimestampMutation) =>
+ _inbound.TryAcceptDeferredMotion(
+ update,
+ out timestamps,
+ out hasTimestampMutation);
+
+ internal bool TryAcceptDeferredState(
+ SetState.Parsed update,
+ out AcceptedPhysicsTimestamps timestamps) =>
+ _inbound.TryAcceptDeferredState(update, out timestamps);
+
+ internal bool TryAcceptDeferredVector(
+ VectorUpdate.Parsed update,
+ out AcceptedPhysicsTimestamps timestamps) =>
+ _inbound.TryAcceptDeferredVector(update, out timestamps);
+
public bool IsFreshTeleportStart(uint guid, ushort teleportSequence) =>
_inbound.IsFreshTeleportStart(guid, teleportSequence);
diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
index 3c377d6c..a238d27a 100644
--- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
+++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
@@ -1,3 +1,4 @@
+using System.Collections.Immutable;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
@@ -11,7 +12,8 @@ public readonly record struct RuntimeEntityRegistrationResult(
RuntimeEntityRecord? Canonical,
bool LogicalRegistrationCreated,
bool ReplacedExistingGeneration,
- Exception? PriorGenerationCleanupFailure = null);
+ Exception? PriorGenerationCleanupFailure = null,
+ bool DeferredForParent = false);
public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
int ActiveEntityCount,
@@ -19,6 +21,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
int ClaimedLocalIdCount,
int AcceptedSnapshotCount,
int UnresolvedParentRelationCount,
+ int DeferredParentCreateCount,
int StagedParentRelationCount,
int RecoveryParentRelationCount,
int CommittedParentRelationCount,
@@ -44,6 +47,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
&& ClaimedLocalIdCount == 0
&& AcceptedSnapshotCount == 0
&& UnresolvedParentRelationCount == 0
+ && DeferredParentCreateCount == 0
&& StagedParentRelationCount == 0
&& RecoveryParentRelationCount == 0
&& CommittedParentRelationCount == 0
@@ -205,6 +209,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.ClaimedLocalIdCount,
Entities.Snapshots.Count,
parents.UnresolvedRelationCount,
+ parents.DeferredCreateCount,
parents.StagedRelationCount,
parents.RecoveryRelationCount,
parents.CommittedRelationCount,
@@ -272,6 +277,35 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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
@@ -285,7 +319,44 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
$"CreateObject 0x{incoming.Guid:X8} cannot acquire a structurally valid initial residence lease.");
}
- InboundCreateResult result = Entities.AcceptCreate(incoming);
+ 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)
{
@@ -307,6 +378,33 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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
@@ -592,6 +690,33 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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(
@@ -617,6 +742,33 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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(
@@ -655,6 +807,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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,
@@ -669,6 +829,44 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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,
@@ -750,6 +948,39 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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,
@@ -790,6 +1021,35 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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(
@@ -817,6 +1077,35 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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
@@ -883,24 +1172,58 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
out AcceptedPhysicsTimestamps timestamps)
{
EnsureNotDisposed();
- RuntimeInitialCreateResidenceLease priorInitialResidence = default;
- bool hadPendingInitialResidence =
- Entities.TryGetActive(
+ if (TryGetPendingInitialResidence(
update.Guid,
- out RuntimeEntityRecord pendingCanonical)
- && InitialCreateResidences.TryGetTransaction(
- pendingCanonical,
- out priorInitialResidence);
- if (hadPendingInitialResidence
- && !InitialCreateResidences.CanEnqueueAcceptedPosition(
- pendingCanonical,
- priorInitialResidence,
- update))
+ out RuntimeEntityRecord pendingCanonical,
+ out RuntimeInitialCreateResidenceLease pendingLease))
{
- disposition = PositionTimestampDisposition.Rejected;
- accepted = default;
- timestamps = default;
- return false;
+ if (!RuntimeAuthoritativePositionRouteClassifier
+ .IsValidCreateWirePosition(update.Position)
+ || update.Velocity is { } velocity
+ && !IsFinite(velocity)
+ || !InitialCreateResidences.CanEnqueue(
+ pendingCanonical,
+ pendingLease))
+ {
+ disposition = PositionTimestampDisposition.Rejected;
+ accepted = default;
+ timestamps = default;
+ return false;
+ }
+
+ bool deferredKnown = Entities.TryAcceptDeferredPosition(
+ update,
+ isLocalPlayer,
+ out disposition,
+ out timestamps,
+ out bool timestampMutation);
+ accepted = pendingCanonical.Snapshot;
+ if (!deferredKnown)
+ return false;
+
+ if (disposition is PositionTimestampDisposition.Rejected
+ && !timestampMutation)
+ {
+ return true;
+ }
+
+ EnqueueDormant(
+ pendingCanonical,
+ pendingLease,
+ RuntimeInitialCreateContinuationKind.Position,
+ RuntimeAcceptedPositionSource.PositionEvent,
+ new RuntimeInitialCreateTailAction(
+ RuntimeInitialCreateTailActionKind.Position,
+ update.Guid,
+ Position: update,
+ PositionSource:
+ RuntimeAcceptedPositionSource.PositionEvent,
+ PositionDisposition: disposition,
+ PreviousTeleportSequence:
+ timestamps.PreviousTeleport,
+ AcceptedTimestamps: timestamps,
+ HasTimestampMutation: timestampMutation));
+ return true;
}
bool hadCanonical = Entities.TryGetActive(
update.Guid,
@@ -938,43 +1261,6 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|| projectionRequiresTeleportHook,
};
}
-
-
- // Initial CreateObject residence is one immutable transaction. A
- // Position packet accepted before its 0x11 placement completes is
- // retained in the transaction's ordered continuation batch; it must
- // not replace the initial mover operation, mutate the Runtime record
- // used to prepare that operation, or escape through a host callback.
- if (hadPendingInitialResidence)
- {
- if (!acceptedPosition)
- return true;
- if (!ReferenceEquals(canonical, pendingCanonical))
- throw FailInitialResidenceRegistration(
- canonical,
- publishDeleted: true);
-
- RuntimeInitialCreateResidenceLease retained =
- InitialCreateResidences.EnqueueAcceptedPosition(
- canonical,
- priorInitialResidence,
- update,
- accepted,
- disposition,
- timestamps,
- isLocalPlayer,
- forcePositionRotation,
- currentLocalVelocity,
- projectionRequiresTeleportHook);
- if (!retained.IsValid)
- {
- throw FailInitialResidenceRegistration(
- canonical,
- publishDeleted: true);
- }
- return true;
- }
-
RuntimePlacementCancellationReceipt cancellation = default;
if (acceptedPosition)
{
@@ -1118,6 +1404,15 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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!;
@@ -1414,6 +1709,288 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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);
diff --git a/src/AcDream.Runtime/Entities/RuntimeInitialCreateAdmissionFreezer.cs b/src/AcDream.Runtime/Entities/RuntimeInitialCreateAdmissionFreezer.cs
new file mode 100644
index 00000000..d63ecedf
--- /dev/null
+++ b/src/AcDream.Runtime/Entities/RuntimeInitialCreateAdmissionFreezer.cs
@@ -0,0 +1,92 @@
+using System.Collections.Immutable;
+using AcDream.Core.Net;
+using AcDream.Core.Net.Messages;
+
+namespace AcDream.Runtime.Entities;
+
+///
+/// Takes ownership of decoded wire collections retained beyond the inbound
+/// callback. The protocol records expose read-only views, but those views may
+/// still wrap parser-owned arrays. Initial-placement admission must therefore
+/// copy every collection before an asynchronous residence lease can retain it.
+///
+internal static class RuntimeInitialCreateAdmissionFreezer
+{
+ internal static WorldSession.EntitySpawn Freeze(
+ in WorldSession.EntitySpawn spawn) => spawn with
+ {
+ AnimPartChanges = spawn.AnimPartChanges.ToImmutableArray(),
+ TextureChanges = spawn.TextureChanges.ToImmutableArray(),
+ SubPalettes = spawn.SubPalettes.ToImmutableArray(),
+ MotionState = Freeze(spawn.MotionState),
+ Physics = Freeze(spawn.Physics),
+ };
+
+ internal static ObjDescEvent.Parsed Freeze(
+ in ObjDescEvent.Parsed update) => update with
+ {
+ ModelData = Freeze(update.ModelData),
+ };
+
+ internal static WorldSession.EntityMotionUpdate Freeze(
+ in WorldSession.EntityMotionUpdate update) => update with
+ {
+ MotionState = Freeze(update.MotionState),
+ };
+
+ internal static RuntimeInitialCreateTailAction Freeze(
+ in RuntimeInitialCreateTailAction action) => action with
+ {
+ Description = Freeze(action.Description),
+ ObjDesc = action.ObjDesc is { } objDesc
+ ? Freeze(objDesc)
+ : null,
+ Movement = action.Movement is { } movement
+ ? Freeze(movement)
+ : null,
+ WeenieDescription = action.WeenieDescription is { } spawn
+ ? Freeze(spawn)
+ : null,
+ };
+
+ private static CreateObject.ModelData Freeze(
+ in CreateObject.ModelData model) => model with
+ {
+ SubPalettes = model.SubPalettes.ToImmutableArray(),
+ TextureChanges = model.TextureChanges.ToImmutableArray(),
+ AnimPartChanges = model.AnimPartChanges.ToImmutableArray(),
+ };
+
+ private static CreateObject.ServerMotionState Freeze(
+ in CreateObject.ServerMotionState motion) => motion with
+ {
+ Commands = motion.Commands?.ToImmutableArray(),
+ };
+
+ private static CreateObject.ServerMotionState? Freeze(
+ CreateObject.ServerMotionState? motion) => motion is { } value
+ ? Freeze(value)
+ : null;
+
+ private static PhysicsSpawnData? Freeze(PhysicsSpawnData? physics)
+ {
+ if (physics is not { } value)
+ return null;
+
+ PhysicsMovementData? movement = value.Movement is { } source
+ ? source with
+ {
+ RawData = source.RawData.ToArray(),
+ MotionState = Freeze(source.MotionState),
+ }
+ : null;
+ ReadOnlyMemory? children = value.Children is { } list
+ ? list.ToArray()
+ : null;
+ return value with
+ {
+ Movement = movement,
+ Children = children,
+ };
+ }
+}
diff --git a/src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs b/src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs
index f35fa9f5..c8e1388c 100644
--- a/src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs
+++ b/src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs
@@ -30,11 +30,14 @@ internal readonly record struct RuntimeInitialCreateResidenceLease(
RuntimeInitialCreateResidenceToken Token,
RuntimeAuthoritativePositionRoute Route,
RuntimeEntityPlacementToken Placement,
+ WorldSession.EntitySpawn InitialCreate,
ImmutableArray Continuations)
{
internal bool IsValid => Token.IsValid
&& Route.Accepted
&& Route.Authority.Entity == Token.Entity
+ && InitialCreate.Guid != 0u
+ && InitialCreate.InstanceSequence == Token.Entity.Incarnation
&& !Continuations.IsDefault
&& HasValidContinuationChain()
&& (!Route.PerformsSetPosition
@@ -43,7 +46,7 @@ internal readonly record struct RuntimeInitialCreateResidenceLease(
private bool HasValidContinuationChain()
{
- ushort previousTeleport = Route.Authority.AcceptedTeleportSequence;
+ uint ownerGuid = InitialCreate.Guid;
for (int index = 0; index < Continuations.Length; index++)
{
RuntimeInitialCreateResidenceContinuation continuation =
@@ -51,52 +54,291 @@ internal readonly record struct RuntimeInitialCreateResidenceLease(
if (!continuation.IsValid
|| continuation.Sequence != (ulong)index + 1UL
|| continuation.InstanceSequence != Token.Entity.Incarnation
- || continuation.PositionAuthorityVersion
- != Token.PositionAuthorityVersion
- || continuation.PreviousTeleportSequence
- != previousTeleport)
+ || continuation.Actions.Any(
+ action => action.OwnerGuid != ownerGuid))
{
return false;
}
- previousTeleport = continuation.AcceptedTeleportSequence;
}
return true;
}
}
+internal enum RuntimeInitialCreateContinuationKind : byte
+{
+ SameIncarnationCreate,
+ ObjDesc,
+ Parent,
+ Pickup,
+ Position,
+ Movement,
+ State,
+ Vector,
+}
+
+internal enum RuntimeInitialCreateTailActionKind : byte
+{
+ ///
+ /// Isolated AP-119 compatibility behavior. Retail's equal-generation
+ /// Create tail starts at ObjDesc and does not call set_description again.
+ ///
+ PreTailDescriptionAdaptation,
+ ObjDesc,
+ CreateParent,
+ Parent,
+ Pickup,
+ Position,
+ Movement,
+ State,
+ Vector,
+ WeenieDescription,
+ ResidentCellCleanup,
+}
+
+internal readonly record struct RuntimeInitialCreateTailAction(
+ RuntimeInitialCreateTailActionKind Kind,
+ uint OwnerGuid,
+ PhysicsSpawnData? Description = null,
+ ObjDescEvent.Parsed? ObjDesc = null,
+ CreateParentUpdate? CreateParent = null,
+ ParentEvent.Parsed? Parent = null,
+ PickupEvent.Parsed? Pickup = null,
+ WorldSession.EntityPositionUpdate? Position = null,
+ WorldSession.EntityMotionUpdate? Movement = null,
+ SetState.Parsed? State = null,
+ VectorUpdate.Parsed? Vector = null,
+ WorldSession.EntitySpawn? WeenieDescription = null,
+ RuntimeAcceptedPositionSource PositionSource =
+ RuntimeAcceptedPositionSource.Unknown,
+ PositionTimestampDisposition PositionDisposition =
+ PositionTimestampDisposition.Rejected,
+ ushort PreviousTeleportSequence = 0,
+ AcceptedPhysicsTimestamps AcceptedTimestamps = default,
+ bool AppliesMovementPayload = false,
+ bool RetainMovementPayload = true,
+ bool HasTimestampMutation = false)
+{
+ internal bool IsStructurallyValid => HasExclusivePayload()
+ && Kind switch
+ {
+ RuntimeInitialCreateTailActionKind.PreTailDescriptionAdaptation =>
+ Description is not null,
+ RuntimeInitialCreateTailActionKind.ObjDesc => ObjDesc is { } objDesc
+ && objDesc.Guid == OwnerGuid,
+ RuntimeInitialCreateTailActionKind.CreateParent =>
+ CreateParent is { } createParent
+ && createParent.ChildGuid == OwnerGuid,
+ RuntimeInitialCreateTailActionKind.Parent => Parent is { } parent
+ && parent.ChildGuid == OwnerGuid,
+ RuntimeInitialCreateTailActionKind.Pickup => Pickup is { } pickup
+ && pickup.Guid == OwnerGuid,
+ RuntimeInitialCreateTailActionKind.Position => Position is { } position
+ && position.Guid == OwnerGuid
+ && PositionSource is RuntimeAcceptedPositionSource.PositionEvent
+ or RuntimeAcceptedPositionSource.SameIncarnationCreate
+ && (PositionDisposition is PositionTimestampDisposition.Apply
+ or PositionTimestampDisposition.ForcePosition
+ || PositionDisposition is PositionTimestampDisposition.Rejected
+ && HasTimestampMutation),
+ RuntimeInitialCreateTailActionKind.Movement => Movement is { } movement
+ && movement.Guid == OwnerGuid
+ && (AppliesMovementPayload || HasTimestampMutation),
+ RuntimeInitialCreateTailActionKind.State => State is { } state
+ && state.Guid == OwnerGuid,
+ RuntimeInitialCreateTailActionKind.Vector => Vector is { } vector
+ && vector.Guid == OwnerGuid,
+ RuntimeInitialCreateTailActionKind.WeenieDescription =>
+ WeenieDescription is { } weenie
+ && weenie.Guid == OwnerGuid,
+ RuntimeInitialCreateTailActionKind.ResidentCellCleanup => true,
+ _ => false,
+ };
+
+ internal bool MatchesInstance(ushort instanceSequence) => Kind switch
+ {
+ RuntimeInitialCreateTailActionKind.PreTailDescriptionAdaptation =>
+ Description is { } description
+ && description.Timestamps.Instance == instanceSequence,
+ RuntimeInitialCreateTailActionKind.ObjDesc =>
+ ObjDesc is { } objDesc
+ && objDesc.InstanceSequence == instanceSequence,
+ RuntimeInitialCreateTailActionKind.CreateParent =>
+ CreateParent is { } createParent
+ && createParent.ChildInstanceSequence == instanceSequence,
+ // ParentEvent carries only the parent's INSTANCE_TS. Acceptance
+ // already exact-gated the child's current incarnation; preserve that
+ // proof in AcceptedTimestamps for the later no-regate executor.
+ RuntimeInitialCreateTailActionKind.Parent =>
+ AcceptedTimestamps.Instance == instanceSequence,
+ RuntimeInitialCreateTailActionKind.Pickup =>
+ Pickup is { } pickup
+ && pickup.InstanceSequence == instanceSequence,
+ RuntimeInitialCreateTailActionKind.Position =>
+ Position is { } position
+ && position.InstanceSequence == instanceSequence,
+ RuntimeInitialCreateTailActionKind.Movement =>
+ Movement is { } movement
+ && movement.InstanceSequence == instanceSequence,
+ RuntimeInitialCreateTailActionKind.State =>
+ State is { } state
+ && state.InstanceSequence == instanceSequence,
+ RuntimeInitialCreateTailActionKind.Vector =>
+ Vector is { } vector
+ && vector.InstanceSequence == instanceSequence,
+ RuntimeInitialCreateTailActionKind.WeenieDescription =>
+ WeenieDescription is { } weenie
+ && weenie.InstanceSequence == instanceSequence,
+ RuntimeInitialCreateTailActionKind.ResidentCellCleanup => true,
+ _ => false,
+ };
+
+ private bool HasExclusivePayload()
+ {
+ int payloadCount = (Description is null ? 0 : 1)
+ + (ObjDesc is null ? 0 : 1)
+ + (CreateParent is null ? 0 : 1)
+ + (Parent is null ? 0 : 1)
+ + (Pickup is null ? 0 : 1)
+ + (Position is null ? 0 : 1)
+ + (Movement is null ? 0 : 1)
+ + (State is null ? 0 : 1)
+ + (Vector is null ? 0 : 1)
+ + (WeenieDescription is null ? 0 : 1);
+ return Kind is RuntimeInitialCreateTailActionKind.ResidentCellCleanup
+ ? payloadCount == 0
+ : payloadCount == 1;
+ }
+}
+
///
-/// One accepted Position packet which arrived before the immutable initial
-/// CreateObject admission completed. These records remain dormant until a
-/// host adopts the completed batch; accepting them never replaces or mutates
-/// the initial 0x11 SetPosition operation.
+/// One immutable inbound envelope which arrived while retail's synchronous
+/// CreateObject critical section was virtualized by an asynchronous initial
+/// SetPosition. Envelopes are never coalesced. A same-incarnation Create is
+/// one atomic envelope whose ordered tail is expanded only by the executor.
+/// Position retains the raw PositionPack and is classified only at the FIFO
+/// head, after every earlier envelope has committed.
///
internal readonly record struct RuntimeInitialCreateResidenceContinuation(
ulong Sequence,
- RuntimePositionEntityKind EntityKind,
+ RuntimeInitialCreateContinuationKind Kind,
ushort InstanceSequence,
- ushort PositionSequence,
- ushort PreviousTeleportSequence,
- ushort TeleportSequence,
- ushort ForcePositionSequence,
- ushort AcceptedTeleportSequence,
- ushort AcceptedForcePositionSequence,
- PositionTimestampDisposition TimestampDisposition,
- CreateObject.ServerPosition AcceptedWirePosition,
- Vector3? PositionPackVelocity,
- Vector3? AcceptedVelocity,
- Quaternion? ForcePositionRotation,
- Vector3? CurrentLocalVelocity,
- bool ProjectionRequiresTeleportHook,
- uint PlacementFrame,
- ulong PositionAuthorityVersion)
+ RuntimeAcceptedPositionSource PositionSource,
+ ImmutableArray Actions)
{
internal bool IsValid => Sequence != 0UL
- && EntityKind is RuntimePositionEntityKind.LocalPlayer
- or RuntimePositionEntityKind.Remote
- or RuntimePositionEntityKind.Projectile
- && PositionAuthorityVersion != 0UL
- && TimestampDisposition is PositionTimestampDisposition.Apply
- or PositionTimestampDisposition.ForcePosition;
+ && !Actions.IsDefaultOrEmpty
+ && Actions.All(static action => action.IsStructurallyValid)
+ && HasMatchingInstances()
+ && HasValidShape();
+
+ private bool HasMatchingInstances()
+ {
+ for (int index = 0; index < Actions.Length; index++)
+ {
+ if (!Actions[index].MatchesInstance(InstanceSequence))
+ return false;
+ }
+ return true;
+ }
+
+ private bool HasValidShape()
+ {
+ if (Kind is not RuntimeInitialCreateContinuationKind.SameIncarnationCreate)
+ {
+ if (Actions.Length != 1)
+ return false;
+ RuntimeInitialCreateTailAction action = Actions[0];
+ RuntimeInitialCreateTailActionKind expected = Kind switch
+ {
+ RuntimeInitialCreateContinuationKind.ObjDesc =>
+ RuntimeInitialCreateTailActionKind.ObjDesc,
+ RuntimeInitialCreateContinuationKind.Parent =>
+ RuntimeInitialCreateTailActionKind.Parent,
+ RuntimeInitialCreateContinuationKind.Pickup =>
+ RuntimeInitialCreateTailActionKind.Pickup,
+ RuntimeInitialCreateContinuationKind.Position =>
+ RuntimeInitialCreateTailActionKind.Position,
+ RuntimeInitialCreateContinuationKind.Movement =>
+ RuntimeInitialCreateTailActionKind.Movement,
+ RuntimeInitialCreateContinuationKind.State =>
+ RuntimeInitialCreateTailActionKind.State,
+ RuntimeInitialCreateContinuationKind.Vector =>
+ RuntimeInitialCreateTailActionKind.Vector,
+ _ => throw new InvalidOperationException(
+ $"Unsupported initial-Create continuation kind {Kind}."),
+ };
+ return action.Kind == expected
+ && (Kind is RuntimeInitialCreateContinuationKind.Position
+ ? PositionSource is RuntimeAcceptedPositionSource.PositionEvent
+ && action.PositionSource == PositionSource
+ : PositionSource is RuntimeAcceptedPositionSource.Unknown);
+ }
+
+ if (Actions.Length < 2
+ || Actions[^2].Kind
+ is not RuntimeInitialCreateTailActionKind.WeenieDescription
+ || Actions[^1].Kind
+ is not RuntimeInitialCreateTailActionKind.ResidentCellCleanup)
+ {
+ return false;
+ }
+
+ bool hasPosition = Actions.Any(
+ static action => action.Kind
+ is RuntimeInitialCreateTailActionKind.Position);
+ if (hasPosition
+ ? PositionSource
+ is not RuntimeAcceptedPositionSource.SameIncarnationCreate
+ : PositionSource is not RuntimeAcceptedPositionSource.Unknown)
+ {
+ return false;
+ }
+
+ int previousStage = -1;
+ int positionBranchCount = 0;
+ for (int index = 0; index < Actions.Length; index++)
+ {
+ RuntimeInitialCreateTailAction action = Actions[index];
+ int stage = SameCreateStage(action.Kind);
+ if (stage <= previousStage)
+ return false;
+ if (action.Kind is RuntimeInitialCreateTailActionKind.Position
+ && action.PositionSource != PositionSource)
+ {
+ return false;
+ }
+ if (action.Kind is RuntimeInitialCreateTailActionKind.CreateParent
+ or RuntimeInitialCreateTailActionKind.Pickup
+ or RuntimeInitialCreateTailActionKind.Position)
+ {
+ positionBranchCount++;
+ }
+ previousStage = stage;
+ }
+ bool hasPhysicsDescription = Actions.Any(
+ static action => action.Kind
+ is RuntimeInitialCreateTailActionKind
+ .PreTailDescriptionAdaptation);
+ return hasPhysicsDescription
+ ? positionBranchCount <= 1
+ : positionBranchCount == 0;
+ }
+
+ private static int SameCreateStage(RuntimeInitialCreateTailActionKind kind) =>
+ kind switch
+ {
+ RuntimeInitialCreateTailActionKind.PreTailDescriptionAdaptation => 0,
+ RuntimeInitialCreateTailActionKind.ObjDesc => 1,
+ RuntimeInitialCreateTailActionKind.CreateParent
+ or RuntimeInitialCreateTailActionKind.Pickup
+ or RuntimeInitialCreateTailActionKind.Position => 2,
+ RuntimeInitialCreateTailActionKind.Movement => 3,
+ RuntimeInitialCreateTailActionKind.State => 4,
+ RuntimeInitialCreateTailActionKind.Vector => 5,
+ RuntimeInitialCreateTailActionKind.WeenieDescription => 6,
+ RuntimeInitialCreateTailActionKind.ResidentCellCleanup => 7,
+ _ => int.MinValue,
+ };
}
internal readonly record struct RuntimeInitialCreateResidenceAdoptionToken(
@@ -259,125 +501,37 @@ internal sealed class RuntimeInitialCreateResidenceState
return Own(record, route);
}
- internal RuntimeInitialCreateResidenceLease EnqueueAcceptedPosition(
+ internal RuntimeInitialCreateResidenceLease EnqueueAccepted(
RuntimeEntityRecord record,
in RuntimeInitialCreateResidenceLease prior,
- WorldSession.EntityPositionUpdate update,
- in WorldSession.EntitySpawn accepted,
- PositionTimestampDisposition disposition,
- in AcceptedPhysicsTimestamps timestamps,
- bool isLocalPlayer,
- Quaternion? forcePositionRotation,
- Vector3? currentLocalVelocity,
- bool projectionRequiresTeleportHook)
+ RuntimeInitialCreateContinuationKind kind,
+ RuntimeAcceptedPositionSource positionSource,
+ ImmutableArray actions)
{
- ArgumentNullException.ThrowIfNull(record);
- if (!_entities.IsCurrent(record)
- || record.Key is not { } key
- || update.Guid != record.ServerGuid
- || accepted.Guid != record.ServerGuid
- || disposition is PositionTimestampDisposition.Rejected)
+ var owned = ImmutableArray.CreateBuilder<
+ RuntimeInitialCreateTailAction>(actions.Length);
+ foreach (RuntimeInitialCreateTailAction action in actions)
{
- return default;
+ owned.Add(RuntimeInitialCreateAdmissionFreezer.Freeze(action));
}
-
- RuntimeInitialCreateResidenceLease current;
- Entry? active = null;
- CompletedEntry? completed = null;
- if (_entries.TryGetValue(key, out active)
- && ReferenceEquals(active.Record, record)
- && active.Lease.Token == prior.Token
- && IsCurrent(active))
- {
- current = active.Lease;
- }
- else if (_completed.TryGetValue(key, out completed)
- && ReferenceEquals(completed.Record, record)
- && completed.Lease.Token == prior.Token
- && IsCompletedCurrent(completed))
- {
- current = completed.Lease;
- }
- else
- {
- return default;
- }
- if (current.Continuations.Length == int.MaxValue
- || completed is not null
- && completed.Receipt.Adoption.Revision == ulong.MaxValue)
- return default;
-
- RuntimePositionEntityKind entityKind = isLocalPlayer
- ? RuntimePositionEntityKind.LocalPlayer
- : (record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0
- ? RuntimePositionEntityKind.Projectile
- : RuntimePositionEntityKind.Remote;
- CreateObject.ServerPosition acceptedPosition =
- accepted.Position ?? update.Position;
- ushort previousTeleport = current.Continuations.IsEmpty
- ? current.Route.Authority.AcceptedTeleportSequence
- : current.Continuations[^1].AcceptedTeleportSequence;
- ulong sequence = (ulong)current.Continuations.Length + 1UL;
- var continuation = new RuntimeInitialCreateResidenceContinuation(
- sequence,
- entityKind,
- update.InstanceSequence,
- update.PositionSequence,
- previousTeleport,
- update.TeleportSequence,
- update.ForcePositionSequence,
- timestamps.Teleport,
- timestamps.ForcePosition,
- disposition,
- acceptedPosition,
- update.Velocity,
- accepted.Physics?.Velocity,
- forcePositionRotation,
- currentLocalVelocity,
- projectionRequiresTeleportHook,
- accepted.PlacementId ?? 0u,
- record.PositionAuthorityVersion);
- if (!continuation.IsValid)
- return default;
-
- RuntimeInitialCreateResidenceLease revised = current with
- {
- Continuations = current.Continuations.Add(continuation),
- };
- if (active is not null)
- {
- active.Lease = revised;
- }
- else
- {
- completed!.Lease = revised;
- RuntimeInitialCreateResidenceAdoptionToken revisedAdoption =
- completed.Receipt.Adoption with
- {
- Revision = completed.Receipt.Adoption.Revision + 1UL,
- };
- completed.Receipt = completed.Receipt with
- {
- Adoption = revisedAdoption,
- Continuations = revised.Continuations,
- };
- }
- return revised;
+ return Enqueue(
+ record,
+ prior,
+ new RuntimeInitialCreateResidenceContinuation(
+ NextSequence(prior),
+ kind,
+ record.Incarnation,
+ positionSource,
+ owned.MoveToImmutable()));
}
- internal bool CanEnqueueAcceptedPosition(
+ internal bool CanEnqueue(
RuntimeEntityRecord record,
- in RuntimeInitialCreateResidenceLease prior,
- in WorldSession.EntityPositionUpdate update)
+ in RuntimeInitialCreateResidenceLease prior)
{
ArgumentNullException.ThrowIfNull(record);
- if (record.Key is not { } key
- || update.Guid != record.ServerGuid
- || !RuntimeAuthoritativePositionRouteClassifier
- .IsValidCreateWirePosition(update.Position))
- {
+ if (record.Key is not { } key)
return false;
- }
if (_entries.TryGetValue(key, out Entry? entry))
{
return ReferenceEquals(entry.Record, record)
@@ -393,6 +547,61 @@ internal sealed class RuntimeInitialCreateResidenceState
&& completed.Receipt.Adoption.Revision < ulong.MaxValue;
}
+ private RuntimeInitialCreateResidenceLease Enqueue(
+ RuntimeEntityRecord record,
+ in RuntimeInitialCreateResidenceLease prior,
+ in RuntimeInitialCreateResidenceContinuation continuation)
+ {
+ ArgumentNullException.ThrowIfNull(record);
+ if (!continuation.IsValid
+ || continuation.InstanceSequence != record.Incarnation
+ || !CanEnqueue(record, prior))
+ {
+ return default;
+ }
+
+ RuntimeEntityKey key = record.Key!.Value;
+ Entry? active = null;
+ CompletedEntry? completed = null;
+ RuntimeInitialCreateResidenceLease current;
+ if (_entries.TryGetValue(key, out active))
+ current = active.Lease;
+ else if (_completed.TryGetValue(key, out completed))
+ current = completed.Lease;
+ else
+ return default;
+
+ if (continuation.Sequence != NextSequence(current))
+ return default;
+ RuntimeInitialCreateResidenceLease revised = current with
+ {
+ Continuations = current.Continuations.Add(continuation),
+ };
+ if (active is not null)
+ {
+ active.Lease = revised;
+ }
+ else
+ {
+ completed!.Lease = revised;
+ RuntimeInitialCreateResidenceAdoptionToken adoption =
+ completed.Receipt.Adoption with
+ {
+ Revision = completed.Receipt.Adoption.Revision + 1UL,
+ };
+ completed.Receipt = completed.Receipt with
+ {
+ Adoption = adoption,
+ Continuations = revised.Continuations,
+ };
+ }
+ return revised;
+ }
+
+ private static ulong NextSequence(
+ in RuntimeInitialCreateResidenceLease lease) =>
+ (ulong)lease.Continuations.Length + 1UL;
+
internal bool TryGetTransaction(
RuntimeEntityRecord record,
out RuntimeInitialCreateResidenceLease lease)
@@ -457,6 +666,7 @@ internal sealed class RuntimeInitialCreateResidenceState
token,
route,
placement,
+ RuntimeInitialCreateAdmissionFreezer.Freeze(record.Snapshot),
ImmutableArray.Empty);
_entries.Add(key, new Entry
{
@@ -600,6 +810,12 @@ internal sealed class RuntimeInitialCreateResidenceState
Retire(current);
return false;
}
+ // Admission-only checkpoint: a completed initial placement remains
+ // owned until the next slice's continuation executor has drained the
+ // exact FIFO revision. Letting a host acknowledge here would silently
+ // discard accepted packets.
+ if (!current.Lease.Continuations.IsEmpty)
+ return false;
if (current.Lease.Route.PerformsSetPosition
&& !_setPosition.ConsumeAcknowledgedPlacement(
current.Lease.Placement,
diff --git a/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs b/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs
index 28c70f8e..3d90357b 100644
--- a/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs
+++ b/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs
@@ -54,6 +54,19 @@ internal enum RuntimeTeleportHookPhase : byte
AfterEnterWorld,
}
+///
+/// Exact placement of retail's ConstrainTo relative to the selected
+/// position operation. Local ordinary corrections constrain before their
+/// optional interpolation; teleports and remote hard moves constrain only
+/// after the position operation succeeds.
+///
+internal enum RuntimePositionConstrainPhase : byte
+{
+ None,
+ BeforePositionOperation,
+ AfterPositionOperation,
+}
+
///
/// Exact accepted-wire authority for one presentation-independent position
/// route. identifies the incarnation while
@@ -147,7 +160,7 @@ internal readonly record struct RuntimeAuthoritativePositionRoute(
bool LeaveWorld,
RuntimeTeleportHookPhase TeleportHookPhase,
bool StopInterpolating,
- bool ConstrainAfterRouting,
+ RuntimePositionConstrainPhase ConstrainPhase,
bool PreserveHeading,
bool ZeroVelocity,
bool SendPositionImmediately,
@@ -163,6 +176,12 @@ internal readonly record struct RuntimeAuthoritativePositionRoute(
internal bool RunsTeleportHook =>
TeleportHookPhase is not RuntimeTeleportHookPhase.None;
+
+ internal bool ConstrainBeforeRouting =>
+ ConstrainPhase is RuntimePositionConstrainPhase.BeforePositionOperation;
+
+ internal bool ConstrainAfterRouting =>
+ ConstrainPhase is RuntimePositionConstrainPhase.AfterPositionOperation;
}
///
@@ -214,7 +233,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
LeaveWorld: false,
TeleportHookPhase: RuntimeTeleportHookPhase.None,
StopInterpolating: false,
- ConstrainAfterRouting: false,
+ ConstrainPhase: RuntimePositionConstrainPhase.None,
PreserveHeading: false,
ZeroVelocity: false,
SendPositionImmediately: false,
@@ -246,7 +265,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
? RuntimeTeleportHookPhase.AfterEnterWorld
: RuntimeTeleportHookPhase.None,
StopInterpolating: false,
- ConstrainAfterRouting: false,
+ ConstrainPhase: RuntimePositionConstrainPhase.None,
PreserveHeading: false,
ZeroVelocity: false,
SendPositionImmediately: false,
@@ -286,7 +305,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
LeaveWorld: false,
TeleportHookPhase: RuntimeTeleportHookPhase.None,
StopInterpolating: false,
- ConstrainAfterRouting: false,
+ ConstrainPhase: RuntimePositionConstrainPhase.None,
PreserveHeading: true,
ZeroVelocity: false,
SendPositionImmediately: true,
@@ -309,7 +328,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
LeaveWorld: false,
TeleportHookPhase: RuntimeTeleportHookPhase.AfterPositionOperation,
StopInterpolating: false,
- ConstrainAfterRouting: true,
+ ConstrainPhase: RuntimePositionConstrainPhase.AfterPositionOperation,
PreserveHeading: false,
ZeroVelocity: true,
SendPositionImmediately: false,
@@ -331,7 +350,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
LeaveWorld: false,
TeleportHookPhase: RuntimeTeleportHookPhase.None,
StopInterpolating: false,
- ConstrainAfterRouting: true,
+ ConstrainPhase: RuntimePositionConstrainPhase.BeforePositionOperation,
PreserveHeading: false,
ZeroVelocity: false,
SendPositionImmediately: false,
@@ -357,7 +376,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
LeaveWorld: false,
TeleportHookPhase: RuntimeTeleportHookPhase.BeforePositionOperation,
StopInterpolating: false,
- ConstrainAfterRouting: true,
+ ConstrainPhase: RuntimePositionConstrainPhase.AfterPositionOperation,
PreserveHeading: false,
ZeroVelocity: false,
SendPositionImmediately: false,
@@ -383,7 +402,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
LeaveWorld: false,
TeleportHookPhase: RuntimeTeleportHookPhase.None,
StopInterpolating: false,
- ConstrainAfterRouting: false,
+ ConstrainPhase: RuntimePositionConstrainPhase.None,
PreserveHeading: false,
ZeroVelocity: false,
SendPositionImmediately: false,
@@ -413,7 +432,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
LeaveWorld: false,
TeleportHookPhase: RuntimeTeleportHookPhase.None,
StopInterpolating: !nearby,
- ConstrainAfterRouting: true,
+ ConstrainPhase: RuntimePositionConstrainPhase.AfterPositionOperation,
PreserveHeading: false,
ZeroVelocity: false,
SendPositionImmediately: false,
@@ -445,7 +464,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
LeaveWorld: true,
TeleportHookPhase: RuntimeTeleportHookPhase.None,
StopInterpolating: false,
- ConstrainAfterRouting: false,
+ ConstrainPhase: RuntimePositionConstrainPhase.None,
PreserveHeading: false,
ZeroVelocity: false,
SendPositionImmediately: false,
@@ -532,7 +551,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
LeaveWorld: false,
TeleportHookPhase: RuntimeTeleportHookPhase.None,
StopInterpolating: false,
- ConstrainAfterRouting: false,
+ ConstrainPhase: RuntimePositionConstrainPhase.None,
PreserveHeading: false,
ZeroVelocity: false,
SendPositionImmediately: false,
@@ -552,7 +571,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
LeaveWorld: false,
TeleportHookPhase: RuntimeTeleportHookPhase.None,
StopInterpolating: false,
- ConstrainAfterRouting: false,
+ ConstrainPhase: RuntimePositionConstrainPhase.None,
PreserveHeading: false,
ZeroVelocity: false,
SendPositionImmediately: false,
diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs
index 1ebebabb..046333a4 100644
--- a/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs
+++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs
@@ -1,5 +1,7 @@
+using System.Collections;
using System.Collections.Immutable;
using System.Numerics;
+using System.Reflection;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
@@ -74,6 +76,8 @@ public sealed class RuntimeInitialCreateResidenceStateTests
{
using var lifetime = new RuntimeEntityObjectLifetime();
Bind(lifetime, 8UL);
+ if (parented)
+ _ = lifetime.RegisterEntity(Spawn(0x70004000u, 1));
RuntimeEntityRecord canonical = lifetime
.RegisterEntityWithInitialResidence(
Spawn(
@@ -458,6 +462,7 @@ public sealed class RuntimeInitialCreateResidenceStateTests
{
using var lifetime = new RuntimeEntityObjectLifetime();
Bind(lifetime, 34UL);
+ _ = lifetime.RegisterEntity(Spawn(0x70004020u, 1));
RuntimeEntityRecord canonical = lifetime
.RegisterEntityWithInitialResidence(
Spawn(
@@ -494,6 +499,7 @@ public sealed class RuntimeInitialCreateResidenceStateTests
using var lifetime = new RuntimeEntityObjectLifetime();
Bind(lifetime, 36UL);
const uint guid = 0x70003024u;
+ _ = lifetime.RegisterEntity(Spawn(0x70004024u, 1));
RuntimeEntityRecord canonical = lifetime
.RegisterEntityWithInitialResidence(
Spawn(
@@ -547,18 +553,18 @@ public sealed class RuntimeInitialCreateResidenceStateTests
revised.Adoption.Revision);
RuntimeInitialCreateResidenceContinuation continuation =
Assert.Single(revised.Continuations);
- Assert.Equal(25f, continuation.AcceptedWirePosition.PositionX);
- Assert.Equal((ushort)1, continuation.AcceptedTeleportSequence);
+ RuntimeInitialCreateTailAction position = PositionAction(continuation);
+ Assert.Equal(25f, position.Position!.Value.Position.PositionX);
+ Assert.Equal((ushort)1, position.AcceptedTimestamps.Teleport);
Assert.False(lifetime.AcknowledgeInitialCreateResidenceAdoption(
canonical,
original.Adoption));
- Assert.True(lifetime.AcknowledgeInitialCreateResidenceAdoption(
+ Assert.False(lifetime.AcknowledgeInitialCreateResidenceAdoption(
canonical,
revised.Adoption));
- Assert.Equal(0, lifetime.CaptureOwnership()
+ Assert.Equal(1, lifetime.CaptureOwnership()
.InitialCreateResidenceLeaseCount);
- Assert.True(lifetime.Physics.SetPosition.CaptureOwnership()
- .IsConverged);
+ ConvergeSessionClear(lifetime);
}
[Fact]
@@ -810,6 +816,7 @@ public sealed class RuntimeInitialCreateResidenceStateTests
{
using var lifetime = new RuntimeEntityObjectLifetime();
Bind(lifetime, 17UL);
+ _ = lifetime.RegisterEntity(Spawn(0x70004001u, 1));
RuntimeEntityRecord canonical = lifetime
.RegisterEntityWithInitialResidence(
Spawn(
@@ -885,7 +892,17 @@ public sealed class RuntimeInitialCreateResidenceStateTests
Assert.True(lifetime.TryGetInitialCreateResidence(
canonical,
out RuntimeInitialCreateResidenceLease afterDuplicate));
- Assert.Equal(initial, afterDuplicate);
+ Assert.Equal(initial.Token, afterDuplicate.Token);
+ Assert.Equal(initial.Route, afterDuplicate.Route);
+ Assert.Equal(initial.Placement, afterDuplicate.Placement);
+ RuntimeInitialCreateResidenceContinuation duplicateEnvelope =
+ Assert.Single(afterDuplicate.Continuations);
+ Assert.Equal(RuntimeInitialCreateContinuationKind.SameIncarnationCreate,
+ duplicateEnvelope.Kind);
+ Assert.DoesNotContain(
+ duplicateEnvelope.Actions,
+ static action => action.Kind
+ is RuntimeInitialCreateTailActionKind.Position);
Assert.Equal(positionAuthority, canonical.PositionAuthorityVersion);
Assert.Equal(spatialAuthority, canonical.SpatialAuthorityVersion);
@@ -900,23 +917,9 @@ public sealed class RuntimeInitialCreateResidenceStateTests
Assert.True(lifetime.TryGetInitialCreateResidence(
canonical,
out RuntimeInitialCreateResidenceLease beforeFreshRoute));
- Assert.Equal(initial, beforeFreshRoute);
+ Assert.Equal(2, beforeFreshRoute.Continuations.Length);
Assert.Equal(10f, canonical.Snapshot.Position!.Value.PositionX);
- Assert.NotNull(newer.Inbound.SameGenerationEvents);
-
- WorldSession.EntityPositionUpdate update =
- newer.Inbound.SameGenerationEvents!.Value.Position!.Value;
- Assert.True(lifetime.TryApplyPosition(
- update,
- isLocalPlayer: false,
- forcePositionRotation: null,
- currentLocalVelocity: null,
- projectionRequiresTeleportHook: false,
- acknowledgeProjection: null,
- out PositionTimestampDisposition disposition,
- out _,
- out _));
- Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+ Assert.Null(newer.Inbound.SameGenerationEvents);
Assert.Equal(0u, canonical.FullCellId);
Assert.Equal(10f, canonical.Snapshot.Position!.Value.PositionX);
Assert.True(lifetime.TryGetInitialCreateResidence(
@@ -926,11 +929,12 @@ public sealed class RuntimeInitialCreateResidenceStateTests
Assert.Equal(RuntimeSetPositionOperationKind.RemoteAuthoritative,
successor.Route.OperationKind);
RuntimeInitialCreateResidenceContinuation continuation =
- Assert.Single(successor.Continuations);
- Assert.Equal(30f, continuation.AcceptedWirePosition.PositionX);
- Assert.Equal((ushort)2, continuation.PositionSequence);
+ successor.Continuations[1];
+ RuntimeInitialCreateTailAction position = PositionAction(continuation);
+ Assert.Equal(30f, position.Position!.Value.Position.PositionX);
+ Assert.Equal((ushort)2, position.Position.Value.PositionSequence);
Assert.Equal(PositionTimestampDisposition.Apply,
- continuation.TimestampDisposition);
+ position.PositionDisposition);
Assert.True(successor.Placement.IsValid);
Assert.Equal(1, lifetime.Physics.SetPosition.CaptureOwnership()
.ActiveOperationCount);
@@ -1005,22 +1009,7 @@ public sealed class RuntimeInitialCreateResidenceStateTests
positionSequence: 2,
positionX: 40f),
isLocalPlayer: true);
- WorldSession.EntityPositionUpdate update =
- newer.Inbound.SameGenerationEvents!.Value.Position!.Value;
-
- Assert.True(lifetime.TryApplyPosition(
- update,
- isLocalPlayer: true,
- forcePositionRotation: null,
- currentLocalVelocity: null,
- projectionRequiresTeleportHook: false,
- acknowledgeProjection: null,
- out PositionTimestampDisposition disposition,
- out _,
- out AcceptedPhysicsTimestamps timestamps));
-
- Assert.Equal(PositionTimestampDisposition.Apply, disposition);
- Assert.False(timestamps.TeleportAdvanced);
+ Assert.Null(newer.Inbound.SameGenerationEvents);
Assert.Equal(0u, canonical.FullCellId);
Assert.True(lifetime.TryGetInitialCreateResidence(
canonical,
@@ -1035,10 +1024,12 @@ public sealed class RuntimeInitialCreateResidenceStateTests
Assert.True(successor.Placement.IsValid);
RuntimeInitialCreateResidenceContinuation continuation =
Assert.Single(successor.Continuations);
+ RuntimeInitialCreateTailAction position = PositionAction(continuation);
Assert.Equal(PositionTimestampDisposition.Apply,
- continuation.TimestampDisposition);
- Assert.Equal((ushort)0, continuation.PreviousTeleportSequence);
- Assert.Equal((ushort)0, continuation.TeleportSequence);
+ position.PositionDisposition);
+ Assert.False(position.AcceptedTimestamps.TeleportAdvanced);
+ Assert.Equal((ushort)0, position.PreviousTeleportSequence);
+ Assert.Equal((ushort)0, position.Position!.Value.TeleportSequence);
}
[Fact]
@@ -1059,12 +1050,13 @@ public sealed class RuntimeInitialCreateResidenceStateTests
successor.Route.OperationKind);
RuntimeInitialCreateResidenceContinuation continuation =
Assert.Single(successor.Continuations);
- Assert.Equal(RuntimePositionEntityKind.LocalPlayer,
- continuation.EntityKind);
- Assert.Equal((ushort)1, continuation.ForcePositionSequence);
- Assert.Equal((ushort)0, continuation.PreviousTeleportSequence);
- Assert.Equal((ushort)0, continuation.TeleportSequence);
- Assert.NotNull(continuation.ForcePositionRotation);
+ RuntimeInitialCreateTailAction position = PositionAction(continuation);
+ Assert.Equal(PositionTimestampDisposition.ForcePosition,
+ position.PositionDisposition);
+ Assert.Equal((ushort)1,
+ position.Position!.Value.ForcePositionSequence);
+ Assert.Equal((ushort)0, position.PreviousTeleportSequence);
+ Assert.Equal((ushort)0, position.Position.Value.TeleportSequence);
}
[Fact]
@@ -1083,11 +1075,10 @@ public sealed class RuntimeInitialCreateResidenceStateTests
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
RuntimeInitialCreateResidenceContinuation continuation =
Assert.Single(successor.Continuations);
- Assert.Equal(RuntimePositionEntityKind.LocalPlayer,
- continuation.EntityKind);
- Assert.Equal((ushort)0, continuation.PreviousTeleportSequence);
- Assert.Equal((ushort)1, continuation.TeleportSequence);
- Assert.Equal(Vector3.Zero, continuation.AcceptedVelocity);
+ RuntimeInitialCreateTailAction position = PositionAction(continuation);
+ Assert.Equal((ushort)0, position.PreviousTeleportSequence);
+ Assert.Equal((ushort)1, position.Position!.Value.TeleportSequence);
+ Assert.Null(position.Position.Value.Velocity);
}
[Fact]
@@ -1106,10 +1097,9 @@ public sealed class RuntimeInitialCreateResidenceStateTests
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
RuntimeInitialCreateResidenceContinuation continuation =
Assert.Single(successor.Continuations);
- Assert.Equal(RuntimePositionEntityKind.Remote,
- continuation.EntityKind);
- Assert.Equal((ushort)0, continuation.PreviousTeleportSequence);
- Assert.Equal((ushort)1, continuation.TeleportSequence);
+ RuntimeInitialCreateTailAction position = PositionAction(continuation);
+ Assert.Equal((ushort)0, position.PreviousTeleportSequence);
+ Assert.Equal((ushort)1, position.Position!.Value.TeleportSequence);
}
[Fact]
@@ -1170,17 +1160,17 @@ public sealed class RuntimeInitialCreateResidenceStateTests
retained.Continuations.Select(static item => item.Sequence));
Assert.Equal([20f, 30f, 40f],
retained.Continuations.Select(
- static item => item.AcceptedWirePosition.PositionX));
+ static item => PositionAction(item).Position!.Value.Position.PositionX));
Assert.Equal([(ushort)0, (ushort)0, (ushort)1],
retained.Continuations.Select(
- static item => item.PreviousTeleportSequence));
+ static item => PositionAction(item).PreviousTeleportSequence));
Assert.Equal([(ushort)0, (ushort)1, (ushort)2],
retained.Continuations.Select(
- static item => item.TeleportSequence));
- Assert.False(retained.Continuations[0]
- .ProjectionRequiresTeleportHook);
- Assert.True(retained.Continuations[1]
- .ProjectionRequiresTeleportHook);
+ static item => PositionAction(item).Position!.Value.TeleportSequence));
+ Assert.False(PositionAction(retained.Continuations[0])
+ .AcceptedTimestamps.TeleportHookRequired);
+ Assert.False(PositionAction(retained.Continuations[1])
+ .AcceptedTimestamps.TeleportHookRequired);
}
[Fact]
@@ -1236,7 +1226,8 @@ public sealed class RuntimeInitialCreateResidenceStateTests
canonical,
out retained));
Assert.Equal((ushort)2,
- Assert.Single(retained.Continuations).PositionSequence);
+ PositionAction(Assert.Single(retained.Continuations))
+ .Position!.Value.PositionSequence);
}
[Fact]
@@ -1346,19 +1337,7 @@ public sealed class RuntimeInitialCreateResidenceStateTests
positionSequence: 2,
positionX: 45f),
isLocalPlayer: true);
- WorldSession.EntityPositionUpdate update =
- newer.Inbound.SameGenerationEvents!.Value.Position!.Value;
- Assert.True(lifetime.TryApplyPosition(
- update,
- isLocalPlayer: true,
- forcePositionRotation: null,
- currentLocalVelocity: null,
- projectionRequiresTeleportHook: false,
- acknowledgeProjection: null,
- out PositionTimestampDisposition disposition,
- out _,
- out _));
- Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+ Assert.Null(newer.Inbound.SameGenerationEvents);
Assert.Equal(0, deleted);
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot place));
@@ -1376,7 +1355,8 @@ public sealed class RuntimeInitialCreateResidenceStateTests
successor.Route.TeleportHookPhase);
RuntimeInitialCreateResidenceContinuation continuation =
Assert.Single(successor.Continuations);
- Assert.Equal(45f, continuation.AcceptedWirePosition.PositionX);
+ Assert.Equal(45f, PositionAction(continuation)
+ .Position!.Value.Position.PositionX);
Assert.Equal(
RuntimeInitialCreateResidenceCompletionStatus.Completed,
lifetime.CompleteInitialCreateResidence(
@@ -1406,20 +1386,18 @@ public sealed class RuntimeInitialCreateResidenceStateTests
repeated.Adoption.Revision);
Assert.Equal(2, repeated.Continuations.Length);
Assert.Equal(55f,
- repeated.Continuations[1].AcceptedWirePosition.PositionX);
+ PositionAction(repeated.Continuations[1])
+ .Position!.Value.Position.PositionX);
Assert.False(lifetime.AcknowledgeInitialCreateResidenceAdoption(
canonical,
receipt.Adoption));
- Assert.True(lifetime.AcknowledgeInitialCreateResidenceAdoption(
+ Assert.False(lifetime.AcknowledgeInitialCreateResidenceAdoption(
canonical,
repeated.Adoption));
- Assert.Equal(
- RuntimeInitialCreateResidenceCompletionStatus.RejectedToken,
- lifetime.CompleteInitialCreateResidence(
- canonical,
- successor.Token,
- out _));
+ Assert.Equal(1, lifetime.CaptureOwnership()
+ .InitialCreateResidenceLeaseCount);
Assert.Equal(0, deleted);
+ ConvergeSessionClear(lifetime);
}
[Fact]
@@ -1539,6 +1517,868 @@ public sealed class RuntimeInitialCreateResidenceStateTests
Assert.Equal(1UL, canonical.SpatialAuthorityVersion);
}
+ [Fact]
+ public void MixedAcceptedPacketsRemainOneFrozenArrivalOrderedFifo()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime();
+ Bind(lifetime, 40UL);
+ const uint parentGuid = 0x70004100u;
+ const uint guid = 0x70003100u;
+ _ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1),
+ isLocalPlayer: false)
+ .Canonical!;
+ WorldSession.EntitySpawn frozenCanonical = canonical.Snapshot;
+ Assert.True(lifetime.Entities.TryGetSnapshot(
+ guid,
+ out WorldSession.EntitySpawn frozenWire));
+ ulong lastPublished = lifetime.Events.LastSequence;
+ int callbacks = 0;
+
+ var appearance = new ObjDescEvent.Parsed(
+ guid,
+ new CreateObject.ModelData(
+ 0x04000001u,
+ Array.Empty(),
+ Array.Empty(),
+ Array.Empty()),
+ InstanceSequence: 1,
+ ObjDescSequence: 2);
+ Assert.True(lifetime.TryApplyObjDesc(
+ appearance,
+ _ => callbacks++,
+ out _));
+ Assert.True(lifetime.TryApplyPosition(
+ PositionUpdate(guid, 2, 0, 0, 20f),
+ isLocalPlayer: false,
+ forcePositionRotation: null,
+ currentLocalVelocity: null,
+ projectionRequiresTeleportHook: true,
+ acknowledgeProjection: _ => callbacks++,
+ out _,
+ out _,
+ out _));
+ var motion = new WorldSession.EntityMotionUpdate(
+ guid,
+ new CreateObject.ServerMotionState(0x3d, 0x11),
+ InstanceSequence: 1,
+ MovementSequence: 2,
+ ServerControlSequence: 1,
+ IsAutonomous: false);
+ Assert.True(lifetime.TryApplyMotion(
+ motion,
+ retainPayload: true,
+ acknowledgeProjection: _ => callbacks++,
+ out _,
+ out _));
+ var state = new SetState.Parsed(
+ guid,
+ (uint)(PhysicsStateFlags.Gravity | PhysicsStateFlags.Hidden),
+ InstanceSequence: 1,
+ StateSequence: 2);
+ Assert.True(lifetime.TryApplyState(
+ state,
+ acknowledgeProjection: (_, _) => callbacks++,
+ out _,
+ out _));
+ var vector = new VectorUpdate.Parsed(
+ guid,
+ new Vector3(4f, 5f, 6f),
+ new Vector3(0f, 0f, 0.5f),
+ InstanceSequence: 1,
+ VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(
+ vector,
+ _ => callbacks++,
+ out _));
+ var pickup = new PickupEvent.Parsed(
+ guid,
+ InstanceSequence: 1,
+ PositionSequence: 3);
+ Assert.True(lifetime.TryApplyPickup(
+ pickup,
+ _ => callbacks++,
+ out _));
+ var parent = new ParentEvent.Parsed(
+ parentGuid,
+ guid,
+ ParentLocation: 1u,
+ PlacementId: 7u,
+ ParentInstanceSequence: 1,
+ ChildPositionSequence: 4);
+ Assert.True(lifetime.TryApplyParent(
+ parent,
+ _ => callbacks++,
+ out _));
+
+ WorldSession.EntitySpawn sameCreate = Spawn(
+ guid,
+ 1,
+ positionSequence: 5,
+ positionX: 50f) with
+ {
+ Name = "deferred-description",
+ };
+ PhysicsSpawnData samePhysics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ Physics = samePhysics with
+ {
+ Movement = new PhysicsMovementData(
+ new byte[] { 1 },
+ new CreateObject.ServerMotionState(0x3d, 0x12),
+ IsAutonomous: false),
+ Velocity = new Vector3(7f, 8f, 9f),
+ AngularVelocity = new Vector3(0f, 0f, 1f),
+ Timestamps = samePhysics.Timestamps with
+ {
+ Position = 5,
+ Movement = 3,
+ State = 3,
+ Vector = 3,
+ ObjDesc = 3,
+ },
+ },
+ MovementSequence = 3,
+ };
+ RuntimeEntityRegistrationResult same = lifetime
+ .RegisterEntityWithInitialResidence(
+ sameCreate,
+ isLocalPlayer: false);
+
+ Assert.Equal(CreateObjectTimestampDisposition.ExistingGeneration,
+ same.Inbound.Disposition);
+ Assert.Null(same.Inbound.SameGenerationEvents);
+ Assert.Equal(0, callbacks);
+ Assert.Equal(lastPublished, lifetime.Events.LastSequence);
+ Assert.Equal(frozenCanonical, canonical.Snapshot);
+ Assert.True(lifetime.Entities.TryGetSnapshot(guid, out var wireAfter));
+ Assert.Equal(frozenWire, wireAfter);
+ Assert.False(lifetime.Entities.ParentAttachments.HasCommittedParent(guid));
+
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ Assert.Equal(
+ [
+ RuntimeInitialCreateContinuationKind.ObjDesc,
+ RuntimeInitialCreateContinuationKind.Position,
+ RuntimeInitialCreateContinuationKind.Movement,
+ RuntimeInitialCreateContinuationKind.State,
+ RuntimeInitialCreateContinuationKind.Vector,
+ RuntimeInitialCreateContinuationKind.Pickup,
+ RuntimeInitialCreateContinuationKind.Parent,
+ RuntimeInitialCreateContinuationKind.SameIncarnationCreate,
+ ],
+ lease.Continuations.Select(static item => item.Kind));
+ Assert.Equal(Enumerable.Range(1, 8).Select(static value => (ulong)value),
+ lease.Continuations.Select(static item => item.Sequence));
+ RuntimeInitialCreateResidenceContinuation atomic =
+ lease.Continuations[^1];
+ Assert.Equal(RuntimeAcceptedPositionSource.SameIncarnationCreate,
+ atomic.PositionSource);
+ Assert.Equal(RuntimeInitialCreateTailActionKind.WeenieDescription,
+ atomic.Actions[^2].Kind);
+ Assert.Equal(RuntimeInitialCreateTailActionKind.ResidentCellCleanup,
+ atomic.Actions[^1].Kind);
+ Assert.Equal(50f,
+ PositionAction(atomic).Position!.Value.Position.PositionX);
+ }
+
+ [Fact]
+ public void ZeroInstancePendingResidenceAdmitsSameInstanceContinuation()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime();
+ Bind(lifetime, 48UL);
+ const uint guid = 0x7000310Bu;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 0), false)
+ .Canonical!;
+ WorldSession.EntitySpawn frozen = canonical.Snapshot;
+ ulong eventSequence = lifetime.Events.LastSequence;
+
+ var update = new VectorUpdate.Parsed(
+ guid,
+ new Vector3(2f, 3f, 4f),
+ new Vector3(0f, 0f, 0.5f),
+ InstanceSequence: 0,
+ VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(update, null, out var accepted));
+
+ Assert.Equal(frozen, accepted);
+ Assert.Equal(frozen, canonical.Snapshot);
+ Assert.Equal(eventSequence, lifetime.Events.LastSequence);
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ RuntimeInitialCreateResidenceContinuation queued =
+ Assert.Single(lease.Continuations);
+ Assert.Equal(RuntimeInitialCreateContinuationKind.Vector, queued.Kind);
+ Assert.Equal((ushort)0, Assert.Single(queued.Actions)
+ .Vector!.Value.InstanceSequence);
+ }
+
+ [Fact]
+ public void MissingParentCreateIsRawUnacceptedAndExactDeleteCancelsIt()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime();
+ Bind(lifetime, 41UL);
+ const uint childGuid = 0x70003101u;
+ const uint missingParent = 0x70004101u;
+ var rawParts = new[] { new CreateObject.AnimPartChange(1, 10u) };
+ byte[] rawMovement = [1, 2];
+ WorldSession.EntitySpawn raw = Spawn(
+ childGuid,
+ incarnation: 0,
+ includePosition: false,
+ parentGuid: missingParent);
+ PhysicsSpawnData rawPhysics = raw.Physics!.Value;
+ raw = raw with
+ {
+ AnimPartChanges = rawParts,
+ Physics = rawPhysics with
+ {
+ Movement = new PhysicsMovementData(
+ rawMovement,
+ MotionState: null,
+ IsAutonomous: false),
+ },
+ };
+
+ RuntimeEntityRegistrationResult deferred = lifetime
+ .RegisterEntityWithInitialResidence(raw, isLocalPlayer: false);
+
+ Assert.True(deferred.DeferredForParent);
+ Assert.Null(deferred.Canonical);
+ Assert.Empty(lifetime.Entities.Snapshots);
+ Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ Assert.True(lifetime.Entities.ParentAttachments.ContainsDeferredCreate(
+ childGuid,
+ instanceSequence: 0));
+ rawParts[0] = new CreateObject.AnimPartChange(9, 99u);
+ rawMovement[0] = 99;
+ Assert.True(lifetime.Entities.ParentAttachments.TryPeekDeferredCreate(
+ missingParent,
+ out DeferredParentCreate retained));
+ Assert.Equal((byte)1, retained.Spawn.AnimPartChanges[0].PartIndex);
+ Assert.Equal((byte)1,
+ retained.Spawn.Physics!.Value.Movement!.Value.RawData.Span[0]);
+
+ Assert.False(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(childGuid, InstanceSequence: 0),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out _));
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ Assert.Empty(lifetime.Entities.Snapshots);
+ }
+
+ [Fact]
+ public void DeferredRawChildGenerationFiltersPreserveEqualReplacementAndFuture()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime();
+ Bind(lifetime, 42UL);
+ const uint childGuid = 0x70003102u;
+ const uint missingParent = 0x70004102u;
+ foreach (ushort incarnation in new ushort[] { 1, 2, 3 })
+ {
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(
+ childGuid,
+ incarnation,
+ includePosition: false,
+ parentGuid: missingParent),
+ isLocalPlayer: false).DeferredForParent);
+ }
+
+ // The raw queue belongs to the child CreateObject. Ending or deleting
+ // a parent incarnation must not discard child packets which only name
+ // that parent by GUID and carry no parent incarnation proof.
+ lifetime.Entities.ParentAttachments.EndGeneration(
+ missingParent,
+ replacementGeneration: 7);
+ lifetime.Entities.ParentAttachments.DeleteGeneration(
+ missingParent,
+ deletedGeneration: 7);
+ Assert.Equal(3, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ lifetime.Entities.ParentAttachments.EndGeneration(
+ childGuid,
+ replacementGeneration: 2);
+ Assert.Equal(2, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ Assert.True(lifetime.Entities.ParentAttachments.ContainsDeferredCreate(
+ childGuid,
+ 2));
+ Assert.True(lifetime.Entities.ParentAttachments.ContainsDeferredCreate(
+ childGuid,
+ 3));
+
+ lifetime.Entities.ParentAttachments.DeleteGeneration(
+ childGuid,
+ deletedGeneration: 2);
+ Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ Assert.True(lifetime.Entities.ParentAttachments.ContainsDeferredCreate(
+ childGuid,
+ 3));
+ lifetime.Entities.ParentAttachments.DeleteGeneration(
+ childGuid,
+ deletedGeneration: 3);
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ }
+
+ [Fact]
+ public void MalformedDeferredVectorDoesNotConsumeItsSequence()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime();
+ Bind(lifetime, 43UL);
+ const uint guid = 0x70003103u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), false)
+ .Canonical!;
+ WorldSession.EntitySpawn frozen = canonical.Snapshot;
+ var malformed = new VectorUpdate.Parsed(
+ guid,
+ new Vector3(float.NaN, 0f, 0f),
+ Vector3.Zero,
+ InstanceSequence: 1,
+ VectorSequence: 2);
+ Assert.False(lifetime.TryApplyVector(
+ malformed,
+ acknowledgeProjection: null,
+ out _));
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease before));
+ Assert.Empty(before.Continuations);
+
+ var corrected = malformed with { Velocity = new Vector3(1f, 2f, 3f) };
+ Assert.True(lifetime.TryApplyVector(
+ corrected,
+ acknowledgeProjection: null,
+ out _));
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease after));
+ Assert.Equal(RuntimeInitialCreateContinuationKind.Vector,
+ Assert.Single(after.Continuations).Kind);
+ Assert.Equal(frozen, canonical.Snapshot);
+ Assert.True(lifetime.Entities.TryGetSnapshot(guid, out var wire));
+ Assert.Equal(frozen, wire);
+ }
+
+ [Fact]
+ public void DeleteAndGuidReuseCannotLeakPriorIncarnationFifo()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime();
+ Bind(lifetime, 44UL);
+ const uint guid = 0x70003104u;
+ RuntimeEntityRecord first = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), false)
+ .Canonical!;
+ Assert.True(lifetime.TryApplyVector(
+ new VectorUpdate.Parsed(
+ guid,
+ Vector3.One,
+ Vector3.Zero,
+ InstanceSequence: 1,
+ VectorSequence: 2),
+ acknowledgeProjection: null,
+ out _));
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(guid, 1),
+ isLocalPlayer: false,
+ removeRetainedObject: false,
+ out RuntimeEntityDeleteAcceptance deletion));
+ lifetime.CompleteAcceptedDelete(deletion);
+ Assert.Null(lifetime.RetireCanonicalOnly(first));
+
+ RuntimeEntityRecord replacement = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 2), false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ replacement,
+ out RuntimeInitialCreateResidenceLease lease));
+ Assert.Empty(lease.Continuations);
+ Assert.Equal((ushort)2, replacement.Incarnation);
+ Assert.Equal(1, lifetime.CaptureOwnership()
+ .InitialCreateResidenceLeaseCount);
+ }
+
+ [Fact]
+ public void RetainedAdmissionDeepFreezesEveryParserOwnedCollection()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime();
+ Bind(lifetime, 45UL);
+ const uint guid = 0x70003105u;
+
+ var initialParts = new[] { new CreateObject.AnimPartChange(1, 10u) };
+ var initialTextures = new[]
+ {
+ new CreateObject.TextureChange(2, 20u, 21u),
+ };
+ var initialPalettes = new[]
+ {
+ new CreateObject.SubPaletteSwap(30u, 3, 4),
+ };
+ var initialCommands = new[]
+ {
+ new CreateObject.MotionItem(0x11, 1, 1f),
+ };
+ var movementCommands = new[]
+ {
+ new CreateObject.MotionItem(0x12, 2, 1f),
+ };
+ byte[] rawMovement = [1, 2, 3];
+ var children = new[] { new PhysicsAttachment(0x70004105u, 5u) };
+ WorldSession.EntitySpawn initial = Spawn(guid, 1);
+ PhysicsSpawnData initialPhysics = initial.Physics!.Value;
+ initial = initial with
+ {
+ AnimPartChanges = initialParts,
+ TextureChanges = initialTextures,
+ SubPalettes = initialPalettes,
+ MotionState = new CreateObject.ServerMotionState(
+ 0x3d,
+ 0x10,
+ Commands: initialCommands),
+ Physics = initialPhysics with
+ {
+ Movement = new PhysicsMovementData(
+ rawMovement,
+ new CreateObject.ServerMotionState(
+ 0x3d,
+ 0x10,
+ Commands: movementCommands),
+ IsAutonomous: false),
+ Children = children,
+ },
+ };
+
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(initial, false)
+ .Canonical!;
+ initialParts[0] = new CreateObject.AnimPartChange(9, 99u);
+ initialTextures[0] = new CreateObject.TextureChange(9, 99u, 100u);
+ initialPalettes[0] = new CreateObject.SubPaletteSwap(99u, 9, 9);
+ initialCommands[0] = new CreateObject.MotionItem(0x99, 9, 9f);
+ movementCommands[0] = new CreateObject.MotionItem(0x98, 8, 8f);
+ rawMovement[0] = 99;
+ children[0] = new PhysicsAttachment(0x70009999u, 99u);
+
+ var objParts = new[] { new CreateObject.AnimPartChange(4, 40u) };
+ var objTextures = new[]
+ {
+ new CreateObject.TextureChange(5, 50u, 51u),
+ };
+ var objPalettes = new[]
+ {
+ new CreateObject.SubPaletteSwap(60u, 6, 7),
+ };
+ Assert.True(lifetime.TryApplyObjDesc(
+ new ObjDescEvent.Parsed(
+ guid,
+ new CreateObject.ModelData(
+ 70u,
+ objPalettes,
+ objTextures,
+ objParts),
+ InstanceSequence: 1,
+ ObjDescSequence: 2),
+ acknowledgeProjection: null,
+ out _));
+ objParts[0] = new CreateObject.AnimPartChange(9, 99u);
+ objTextures[0] = new CreateObject.TextureChange(9, 99u, 100u);
+ objPalettes[0] = new CreateObject.SubPaletteSwap(99u, 9, 9);
+
+ var motionCommands = new[]
+ {
+ new CreateObject.MotionItem(0x13, 3, 1f),
+ };
+ Assert.True(lifetime.TryApplyMotion(
+ new WorldSession.EntityMotionUpdate(
+ guid,
+ new CreateObject.ServerMotionState(
+ 0x3d,
+ 0x13,
+ Commands: motionCommands),
+ InstanceSequence: 1,
+ MovementSequence: 2,
+ ServerControlSequence: 1,
+ IsAutonomous: false),
+ retainPayload: true,
+ acknowledgeProjection: null,
+ out _,
+ out _));
+ motionCommands[0] = new CreateObject.MotionItem(0x97, 7, 7f);
+
+ var createParts = new[] { new CreateObject.AnimPartChange(8, 80u) };
+ var createCommands = new[]
+ {
+ new CreateObject.MotionItem(0x14, 4, 1f),
+ };
+ byte[] createRawMovement = [4, 5, 6];
+ WorldSession.EntitySpawn sameCreate = Spawn(
+ guid,
+ 1,
+ positionSequence: 2,
+ positionX: 30f);
+ PhysicsSpawnData samePhysics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ AnimPartChanges = createParts,
+ Physics = samePhysics with
+ {
+ Movement = new PhysicsMovementData(
+ createRawMovement,
+ new CreateObject.ServerMotionState(
+ 0x3d,
+ 0x14,
+ Commands: createCommands),
+ IsAutonomous: false),
+ Timestamps = samePhysics.Timestamps with
+ {
+ ObjDesc = 3,
+ Movement = 3,
+ },
+ },
+ MovementSequence = 3,
+ };
+ Assert.Equal(CreateObjectTimestampDisposition.ExistingGeneration,
+ lifetime.RegisterEntityWithInitialResidence(sameCreate, false)
+ .Inbound.Disposition);
+ createParts[0] = new CreateObject.AnimPartChange(9, 99u);
+ createCommands[0] = new CreateObject.MotionItem(0x96, 6, 6f);
+ createRawMovement[0] = 99;
+
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ Assert.Equal((byte)1, lease.InitialCreate.AnimPartChanges[0].PartIndex);
+ Assert.Equal((byte)2, lease.InitialCreate.TextureChanges[0].PartIndex);
+ Assert.Equal(30u, lease.InitialCreate.SubPalettes[0].SubPaletteId);
+ Assert.Equal((ushort)0x11,
+ lease.InitialCreate.MotionState!.Value.Commands![0].Command);
+ Assert.Equal((byte)1,
+ lease.InitialCreate.Physics!.Value.Movement!.Value.RawData.Span[0]);
+ Assert.Equal((ushort)0x12,
+ lease.InitialCreate.Physics.Value.Movement.Value.MotionState!
+ .Value.Commands![0].Command);
+ Assert.Equal(0x70004105u,
+ lease.InitialCreate.Physics.Value.Children!.Value.Span[0].Guid);
+
+ RuntimeInitialCreateTailAction objDesc = Assert.Single(
+ lease.Continuations[0].Actions);
+ Assert.Equal((byte)4,
+ objDesc.ObjDesc!.Value.ModelData.AnimPartChanges[0].PartIndex);
+ Assert.Equal((byte)5,
+ objDesc.ObjDesc.Value.ModelData.TextureChanges[0].PartIndex);
+ Assert.Equal(60u,
+ objDesc.ObjDesc.Value.ModelData.SubPalettes[0].SubPaletteId);
+ RuntimeInitialCreateTailAction motion = Assert.Single(
+ lease.Continuations[1].Actions);
+ Assert.Equal((ushort)0x13,
+ motion.Movement!.Value.MotionState.Commands![0].Command);
+ RuntimeInitialCreateResidenceContinuation atomic =
+ lease.Continuations[2];
+ RuntimeInitialCreateTailAction description = Assert.Single(
+ atomic.Actions.Where(static action => action.Kind is
+ RuntimeInitialCreateTailActionKind.PreTailDescriptionAdaptation));
+ Assert.Equal((byte)4,
+ description.Description!.Value.Movement!.Value.RawData.Span[0]);
+ Assert.Equal((ushort)0x14,
+ description.Description.Value.Movement.Value.MotionState!
+ .Value.Commands![0].Command);
+ RuntimeInitialCreateTailAction weenie = Assert.Single(
+ atomic.Actions.Where(static action => action.Kind is
+ RuntimeInitialCreateTailActionKind.WeenieDescription));
+ Assert.Equal((byte)8,
+ weenie.WeenieDescription!.Value.AnimPartChanges[0].PartIndex);
+ Assert.Equal((byte)4,
+ weenie.WeenieDescription.Value.Physics!.Value.Movement!.Value
+ .RawData.Span[0]);
+ }
+
+ [Fact]
+ public void CreateIdentityAndParentMismatchFailBeforeAnyAdmissionMutation()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime();
+ Bind(lifetime, 46UL);
+ const uint guid = 0x70003106u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), false)
+ .Canonical!;
+ WorldSession.EntitySpawn frozen = canonical.Snapshot;
+ ulong eventSequence = lifetime.Events.LastSequence;
+
+ WorldSession.EntitySpawn instanceMismatch = Spawn(
+ guid,
+ 1,
+ positionSequence: 2,
+ positionX: 20f);
+ PhysicsSpawnData mismatchedPhysics = instanceMismatch.Physics!.Value;
+ instanceMismatch = instanceMismatch with
+ {
+ Physics = mismatchedPhysics with
+ {
+ Timestamps = mismatchedPhysics.Timestamps with { Instance = 2 },
+ },
+ };
+ Assert.Throws(() => lifetime
+ .RegisterEntityWithInitialResidence(instanceMismatch, false));
+
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease unchanged));
+ Assert.Empty(unchanged.Continuations);
+ Assert.Equal(frozen, canonical.Snapshot);
+ Assert.Equal(eventSequence, lifetime.Events.LastSequence);
+ Assert.True(lifetime.Entities.TryGetSnapshot(guid, out var wire));
+ Assert.Equal(frozen, wire);
+
+ RuntimeEntityRegistrationResult corrected = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, positionSequence: 2, positionX: 20f),
+ false);
+ Assert.Equal(CreateObjectTimestampDisposition.ExistingGeneration,
+ corrected.Inbound.Disposition);
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease admitted));
+ Assert.Equal(RuntimeInitialCreateContinuationKind.SameIncarnationCreate,
+ Assert.Single(admitted.Continuations).Kind);
+
+ const uint child = 0x70003107u;
+ WorldSession.EntitySpawn parentMismatch = Spawn(
+ child,
+ 1,
+ includePosition: false,
+ parentGuid: 0x70004107u);
+ PhysicsSpawnData parentPhysics = parentMismatch.Physics!.Value;
+ parentMismatch = parentMismatch with
+ {
+ Physics = parentPhysics with
+ {
+ Parent = new PhysicsAttachment(0x70004108u, 1u),
+ },
+ };
+ Assert.Throws(() => lifetime
+ .RegisterEntityWithInitialResidence(parentMismatch, false));
+ Assert.False(lifetime.Entities.ParentAttachments.ContainsDeferredCreate(
+ child,
+ 1));
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ WorldSession.EntitySpawn flattenedOnlyParent = Spawn(
+ child,
+ 1,
+ includePosition: false,
+ parentGuid: 0x70004107u);
+ PhysicsSpawnData flattenedOnlyPhysics =
+ flattenedOnlyParent.Physics!.Value;
+ flattenedOnlyParent = flattenedOnlyParent with
+ {
+ Physics = flattenedOnlyPhysics with { Parent = null },
+ };
+ Assert.Throws(() => lifetime
+ .RegisterEntityWithInitialResidence(flattenedOnlyParent, false));
+
+ WorldSession.EntitySpawn physicsOnlyParent = Spawn(
+ child,
+ 1,
+ includePosition: false,
+ parentGuid: 0x70004107u) with
+ {
+ ParentGuid = null,
+ ParentLocation = null,
+ };
+ Assert.Throws(() => lifetime
+ .RegisterEntityWithInitialResidence(physicsOnlyParent, false));
+
+ WorldSession.EntitySpawn noPhysicsTopParent = Spawn(
+ child,
+ 0,
+ includePosition: false,
+ parentGuid: 0x70004107u) with
+ {
+ Physics = null,
+ MovementSequence = 0,
+ ServerControlSequence = 0,
+ PositionSequence = 0,
+ };
+ Assert.Throws(() => lifetime
+ .RegisterEntityWithInitialResidence(noPhysicsTopParent, false));
+ Assert.False(lifetime.Entities.ParentAttachments.ContainsDeferredCreate(
+ child,
+ 0));
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ const uint noPhysicsProjectionGuid = 0x7000310Cu;
+ WorldSession.EntitySpawn noPhysicsSetup = Spawn(
+ noPhysicsProjectionGuid,
+ 0,
+ includePosition: false) with
+ {
+ Physics = null,
+ PhysicsState = null,
+ MovementSequence = 0,
+ ServerControlSequence = 0,
+ PositionSequence = 0,
+ };
+ Assert.Throws(() => lifetime
+ .RegisterEntityWithInitialResidence(noPhysicsSetup, false));
+ Assert.False(lifetime.Entities.TryGetActive(
+ noPhysicsProjectionGuid,
+ out _));
+ Assert.Equal(eventSequence, lifetime.Events.LastSequence);
+
+ WorldSession.EntitySpawn placementMismatch = Spawn(
+ child,
+ 1,
+ includePosition: false,
+ parentGuid: 0x70004107u) with
+ {
+ PlacementId = 7u,
+ };
+ Assert.Throws(() => lifetime
+ .RegisterEntityWithInitialResidence(placementMismatch, false));
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ WorldSession.EntitySpawn timestampMismatch = Spawn(
+ child,
+ 1,
+ includePosition: false,
+ parentGuid: 0x70004107u) with
+ {
+ MovementSequence = 2,
+ };
+ Assert.Throws(() => lifetime
+ .RegisterEntityWithInitialResidence(timestampMismatch, false));
+
+ WorldSession.EntitySpawn positionProjectionMismatch = Spawn(
+ guid,
+ 1,
+ positionSequence: 2,
+ positionX: 20f);
+ PhysicsSpawnData positionProjectionPhysics =
+ positionProjectionMismatch.Physics!.Value;
+ positionProjectionMismatch = positionProjectionMismatch with
+ {
+ Physics = positionProjectionPhysics with
+ {
+ Position = positionProjectionPhysics.Position!.Value with
+ {
+ PositionX = 21f,
+ },
+ },
+ };
+ Assert.Throws(() => lifetime
+ .RegisterEntityWithInitialResidence(
+ positionProjectionMismatch,
+ false));
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease stillUnchanged));
+ Assert.Single(stillUnchanged.Continuations);
+ Assert.Equal(frozen, canonical.Snapshot);
+ Assert.Equal(eventSequence, lifetime.Events.LastSequence);
+
+ WorldSession.EntitySpawn rawInstanceMismatch = Spawn(
+ child,
+ 1,
+ includePosition: false,
+ parentGuid: 0x70004107u);
+ PhysicsSpawnData rawInstancePhysics =
+ rawInstanceMismatch.Physics!.Value;
+ rawInstanceMismatch = rawInstanceMismatch with
+ {
+ Physics = rawInstancePhysics with
+ {
+ Timestamps = rawInstancePhysics.Timestamps with { Instance = 2 },
+ },
+ };
+ Assert.Throws(() => lifetime
+ .RegisterEntityWithInitialResidence(rawInstanceMismatch, false));
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ }
+
+ [Fact]
+ public void DeferredParentCreateAdmissionTokenPreventsResetAbaConsumption()
+ {
+ var state = new ParentAttachmentState();
+ const uint parent = 0x70004109u;
+ WorldSession.EntitySpawn spawn = Spawn(
+ 0x70003109u,
+ 1,
+ includePosition: false,
+ parentGuid: parent);
+ state.EnqueueDeferredCreate(spawn, false);
+ Assert.True(state.TryPeekDeferredCreate(parent, out var oldAdmission));
+ state.Clear();
+ state.EnqueueDeferredCreate(spawn, false);
+ Assert.True(state.TryPeekDeferredCreate(parent, out var newAdmission));
+
+ Assert.NotEqual(oldAdmission.AdmissionId, newAdmission.AdmissionId);
+ Assert.False(state.ConsumeDeferredCreate(parent, oldAdmission));
+ Assert.True(state.ConsumeDeferredCreate(parent, newAdmission));
+ Assert.Equal(0, state.DeferredCreateCount);
+
+ FieldInfo admissionField = typeof(ParentAttachmentState).GetField(
+ "_nextDeferredCreateAdmissionId",
+ BindingFlags.Instance | BindingFlags.NonPublic)!;
+ admissionField.SetValue(state, ulong.MaxValue);
+ Assert.Throws(() =>
+ state.EnqueueDeferredCreate(spawn, false));
+ Assert.Equal(0, state.DeferredCreateCount);
+ }
+
+ [Fact]
+ public void CompletedFifoSaturationFailsBeforeConsumingWireSequence()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime();
+ Bind(lifetime, 47UL);
+ const uint guid = 0x7000310Au;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ Assert.Equal(RuntimeInitialCreateResidenceCompletionStatus.Completed,
+ lifetime.CompleteInitialCreateResidence(
+ canonical,
+ lease.Token,
+ out RuntimeInitialCreateResidenceReceipt receipt));
+
+ SetCompletedAdoptionRevision(
+ lifetime.InitialCreateResidences,
+ canonical.Key!.Value,
+ ulong.MaxValue);
+ var update = new VectorUpdate.Parsed(
+ guid,
+ Vector3.One,
+ Vector3.Zero,
+ InstanceSequence: 1,
+ VectorSequence: 2);
+ Assert.False(lifetime.TryApplyVector(update, null, out _));
+ Assert.Equal(canonical.Snapshot,
+ lifetime.Entities.Snapshots[guid]);
+
+ SetCompletedAdoptionRevision(
+ lifetime.InitialCreateResidences,
+ canonical.Key.Value,
+ receipt.Adoption.Revision);
+ Assert.True(lifetime.TryApplyVector(update, null, out _));
+ Assert.True(lifetime.InitialCreateResidences.TryGetTransaction(
+ canonical,
+ out RuntimeInitialCreateResidenceLease retained));
+ Assert.Equal(RuntimeInitialCreateContinuationKind.Vector,
+ Assert.Single(retained.Continuations).Kind);
+ }
+
private static RuntimeSetPositionCommand Prepare(
RuntimeEntityObjectLifetime lifetime,
in RuntimeInitialCreateResidenceLease lease,
@@ -1559,6 +2399,43 @@ public sealed class RuntimeInitialCreateResidenceStateTests
return command;
}
+ private static RuntimeInitialCreateTailAction PositionAction(
+ in RuntimeInitialCreateResidenceContinuation continuation) =>
+ Assert.Single(continuation.Actions.Where(
+ static action => action.Kind
+ is RuntimeInitialCreateTailActionKind.Position));
+
+ private static void SetCompletedAdoptionRevision(
+ RuntimeInitialCreateResidenceState state,
+ RuntimeEntityKey key,
+ ulong revision)
+ {
+ FieldInfo completedField = typeof(RuntimeInitialCreateResidenceState)
+ .GetField("_completed", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ var completed = (IDictionary)completedField.GetValue(state)!;
+ object entry = completed[key]!;
+ PropertyInfo receiptProperty = entry.GetType().GetProperty(
+ "Receipt",
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!;
+ var receipt = (RuntimeInitialCreateResidenceReceipt)
+ receiptProperty.GetValue(entry)!;
+ receiptProperty.SetValue(
+ entry,
+ receipt with
+ {
+ Adoption = receipt.Adoption with { Revision = revision },
+ });
+ }
+
+ private static void ConvergeSessionClear(
+ RuntimeEntityObjectLifetime lifetime)
+ {
+ foreach (RuntimeEntityRecord record in lifetime.BeginSessionClear())
+ lifetime.CompleteSessionEntityRetirement(record);
+ lifetime.ClearObjects();
+ Assert.True(lifetime.CompleteSessionClearIfConverged());
+ }
+
private static RuntimeInitialCreateResidenceLease ApplyFreshSuccessor(
RuntimeEntityObjectLifetime lifetime,
uint guid,
@@ -1585,27 +2462,12 @@ public sealed class RuntimeInitialCreateResidenceStateTests
teleportSequence: teleportSequence,
forcePositionSequence: forcePositionSequence),
isLocalPlayer);
- WorldSession.EntityPositionUpdate update =
- newer.Inbound.SameGenerationEvents!.Value.Position!.Value;
- var preservedRotation = Quaternion.CreateFromYawPitchRoll(
- 0.5f,
- 0.25f,
- 0.125f);
- var currentVelocity = new Vector3(1f, 2f, 3f);
-
- Assert.True(lifetime.TryApplyPosition(
- update,
- isLocalPlayer,
- forcePositionRotation: preservedRotation,
- currentLocalVelocity: currentVelocity,
- projectionRequiresTeleportHook: true,
- acknowledgeProjection: null,
- out disposition,
- out _,
- out _));
+ Assert.Null(newer.Inbound.SameGenerationEvents);
Assert.True(lifetime.TryGetInitialCreateResidence(
canonical,
out RuntimeInitialCreateResidenceLease retained));
+ disposition = PositionAction(Assert.Single(retained.Continuations))
+ .PositionDisposition;
Assert.Equal(initial.Token, retained.Token);
Assert.Equal(initial.Route, retained.Route);
Assert.Equal(initial.Placement, retained.Placement);
@@ -1767,6 +2629,8 @@ public sealed class RuntimeInitialCreateResidenceStateTests
MotionTableId: null,
PhysicsState: rawState,
InstanceSequence: incarnation,
+ MovementSequence: timestamps.Movement,
+ ServerControlSequence: timestamps.ServerControlledMove,
PositionSequence: positionSequence,
ParentGuid: parentGuid,
ParentLocation: parentGuid is null ? null : 1u,
diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs
index 5db0b5fa..e43f97fd 100644
--- a/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs
+++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs
@@ -140,6 +140,7 @@ public sealed class RuntimeAuthoritativePositionRouteClassifierTests
playerDistance: distance);
Assert.Equal(expected, route.Disposition);
+ Assert.False(route.ConstrainBeforeRouting);
Assert.True(route.ConstrainAfterRouting);
}
@@ -326,7 +327,8 @@ public sealed class RuntimeAuthoritativePositionRouteClassifierTests
: null);
Assert.Equal(expected, route.Disposition);
- Assert.True(route.ConstrainAfterRouting);
+ Assert.True(route.ConstrainBeforeRouting);
+ Assert.False(route.ConstrainAfterRouting);
}
[Theory]