From 30012361e12222e8271b1531574257ba910c77cb Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 1 Aug 2026 21:00:03 +0200 Subject: [PATCH] feat(runtime): freeze initial placement inbound admission --- .../Entities/InboundPhysicsStateController.cs | 216 +++- .../Entities/ParentAttachmentState.cs | 147 +++ .../Entities/RuntimeEntityDirectory.cs | 62 + .../Entities/RuntimeEntityObjectLifetime.cs | 687 ++++++++++- .../RuntimeInitialCreateAdmissionFreezer.cs | 92 ++ .../RuntimeInitialCreateResidenceState.cs | 496 +++++--- ...imeAuthoritativePositionRouteClassifier.cs | 43 +- ...RuntimeInitialCreateResidenceStateTests.cs | 1082 +++++++++++++++-- ...thoritativePositionRouteClassifierTests.cs | 4 +- 9 files changed, 2506 insertions(+), 323 deletions(-) create mode 100644 src/AcDream.Runtime/Entities/RuntimeInitialCreateAdmissionFreezer.cs 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]