diff --git a/src/AcDream.Core/Physics/PhysicsTimestampGate.cs b/src/AcDream.Core/Physics/PhysicsTimestampGate.cs index baae0594..0b9e5ad2 100644 --- a/src/AcDream.Core/Physics/PhysicsTimestampGate.cs +++ b/src/AcDream.Core/Physics/PhysicsTimestampGate.cs @@ -102,6 +102,25 @@ public sealed class PhysicsTimestampGate return CreateObjectTimestampDisposition.ExistingGeneration; } + /// + /// Classifies an incoming CreateObject generation without consuming any + /// channel timestamp. Runtime uses this to preflight only admissions which + /// can create or replace a canonical incarnation; equal/stale packets must + /// still reach the normal per-channel retail gates unchanged. + /// + public CreateObjectTimestampDisposition PreviewCreateObject( + ushort instance) + { + if (!_seeded) + return CreateObjectTimestampDisposition.InitialGeneration; + ushort currentInstance = _timestamps[Instance]; + if (IsNewer(currentInstance, instance)) + return CreateObjectTimestampDisposition.NewGeneration; + if (IsNewer(instance, currentInstance)) + return CreateObjectTimestampDisposition.StaleGeneration; + return CreateObjectTimestampDisposition.ExistingGeneration; + } + public bool TryAcceptMovementEvent( ushort instance, ushort movement, diff --git a/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs b/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs index 2b949929..a88a9058 100644 --- a/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs +++ b/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs @@ -20,6 +20,12 @@ public sealed class InboundPhysicsStateController public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) => _snapshots.TryGetValue(guid, out spawn); + 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) { if (!_gates.TryGetValue(incoming.Guid, out PhysicsTimestampGate? gate)) diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs b/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs index e9b028ae..293f34db 100644 --- a/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs +++ b/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs @@ -47,6 +47,10 @@ public sealed class RuntimeEntityDirectory public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming) => _inbound.AcceptCreate(incoming); + public CreateObjectTimestampDisposition PreviewCreateDisposition( + WorldSession.EntitySpawn incoming) => + _inbound.PreviewCreateDisposition(incoming); + public bool TryDelete( AcDream.Core.Net.Messages.DeleteObject.Parsed delete, bool isLocalPlayer) => diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs index 03dd3cc1..3c377d6c 100644 --- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs +++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs @@ -27,6 +27,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot( int ContainerProjectionCount, int EquipmentOwnerCount, int PendingMoveCount, + int InitialCreateResidenceLeaseCount, int StreamSubscriberCount, int PlacementStreamSubscriberCount, long StreamDispatchFailureCount, @@ -51,6 +52,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot( && ContainerProjectionCount == 0 && EquipmentOwnerCount == 0 && PendingMoveCount == 0 + && InitialCreateResidenceLeaseCount == 0 && StreamSubscriberCount == 0 && PlacementStreamSubscriberCount == 0 && PendingDispatchCount == 0 @@ -118,6 +120,9 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable InventoryView = views.Inventory; Events = new RuntimeEntityObjectEventStream(Entities, Objects); Physics.SetPosition.BindEventStream(Events); + InitialCreateResidences = new RuntimeInitialCreateResidenceState( + Entities, + Physics.SetPosition); Placements = new RuntimePlacementProjectionChannel( Events, Physics.SetPosition); @@ -143,6 +148,9 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable InventoryView = views.Inventory; Events = new RuntimeEntityObjectEventStream(Entities, Objects); Physics.SetPosition.BindEventStream(Events); + InitialCreateResidences = new RuntimeInitialCreateResidenceState( + Entities, + Physics.SetPosition); Placements = new RuntimePlacementProjectionChannel( Events, Physics.SetPosition); @@ -168,6 +176,9 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable InventoryView = views.Inventory; Events = new RuntimeEntityObjectEventStream(Entities, Objects); Physics.SetPosition.BindEventStream(Events); + InitialCreateResidences = new RuntimeInitialCreateResidenceState( + Entities, + Physics.SetPosition); Placements = new RuntimePlacementProjectionChannel( Events, Physics.SetPosition); @@ -180,10 +191,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable public IRuntimeInventoryView InventoryView { get; } public RuntimeEntityObjectEventStream Events { get; } public RuntimePlacementProjectionChannel Placements { get; } + internal RuntimeInitialCreateResidenceState InitialCreateResidences + { get; } public RuntimeEntityObjectOwnershipSnapshot CaptureOwnership() { ParentAttachmentState parents = Entities.ParentAttachments; + RuntimeInitialCreateResidenceOwnershipSnapshot initialResidence = + InitialCreateResidences.CaptureOwnership(); return new RuntimeEntityObjectOwnershipSnapshot( Entities.Count, Entities.PendingTeardownCount, @@ -198,6 +213,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable Objects.ContainerProjectionCount, Objects.EquipmentOwnerCount, Objects.PendingMoveCount, + initialResidence.ActiveLeaseCount + + initialResidence.PendingAdoptionCount, Events.SubscriberCount, Events.PlacementSubscriberCount, Events.DispatchFailureCount, @@ -215,6 +232,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable EnsureNotDisposed(); Events.BindContext(generation, frameNumber); Placements.BindGeneration(generation); + InitialCreateResidences.BindGeneration(generation); } /// @@ -225,7 +243,28 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable /// public RuntimeEntityRegistrationResult RegisterEntity( WorldSession.EntitySpawn incoming, - Func? retirePriorProjection = null) + Func? retirePriorProjection = null) => + RegisterEntityCore( + incoming, + beginInitialResidence: false, + isLocalPlayer: false, + retirePriorProjection); + + internal RuntimeEntityRegistrationResult RegisterEntityWithInitialResidence( + WorldSession.EntitySpawn incoming, + bool isLocalPlayer, + Func? retirePriorProjection = null) => + RegisterEntityCore( + incoming, + beginInitialResidence: true, + isLocalPlayer, + retirePriorProjection); + + private RuntimeEntityRegistrationResult RegisterEntityCore( + WorldSession.EntitySpawn incoming, + bool beginInitialResidence, + bool isLocalPlayer, + Func? retirePriorProjection) { EnsureNotDisposed(); if (_sessionClearInProgress) @@ -233,6 +272,18 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable throw new InvalidOperationException( "A Runtime entity cannot register while its session lifetime is clearing."); } + CreateObjectTimestampDisposition preview = + Entities.PreviewCreateDisposition(incoming); + bool requiresFreshResidenceAdmission = preview is + CreateObjectTimestampDisposition.InitialGeneration + or CreateObjectTimestampDisposition.NewGeneration; + if (beginInitialResidence + && requiresFreshResidenceAdmission + && !InitialCreateResidences.CanAcceptCreate(incoming)) + { + throw new InvalidOperationException( + $"CreateObject 0x{incoming.Guid:X8} cannot acquire a structurally valid initial residence lease."); + } InboundCreateResult result = Entities.AcceptCreate(incoming); if (result.Disposition @@ -256,11 +307,16 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable incoming.Guid, out RuntimeEntityRecord retained)) { + // Existing-generation CreateObject contributes untimestamped + // description fields here. Position/Parent/Pickup/State/etc. + // remain separate freshness-gated events and must not churn a + // pending initial placement or re-preclaim its wire cell. Entities.RefreshSnapshot( retained, result.Snapshot, - refreshPosition: true); - Entities.AdvanceCreateAuthority(retained); + refreshPosition: !beginInitialResidence); + if (!beginInitialResidence) + Entities.AdvanceCreateAuthority(retained); PublishEntity(RuntimeEntityChange.Updated, retained); if (!IsCurrentOperation( incoming.Guid, @@ -291,7 +347,24 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable ReplacedExistingGeneration: false); } + if (beginInitialResidence + && !InitialCreateResidences.CanAcceptCreate(result.Snapshot)) + { + throw new InvalidOperationException( + $"Recovered CreateObject 0x{incoming.Guid:X8} cannot acquire a structurally valid initial residence lease."); + } + RuntimeEntityRecord recovered = Entities.AddActive(result.Snapshot); + if (!InitializeAcceptedCreateResidence( + recovered, + result, + beginInitialResidence, + isLocalPlayer)) + { + throw FailInitialResidenceRegistration( + recovered, + publishDeleted: false); + } PublishEntity(RuntimeEntityChange.Registered, recovered); if (!IsCurrentOperation( incoming.Guid, @@ -366,6 +439,16 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable } RuntimeEntityRecord canonical = Entities.AddActive(result.Snapshot); + if (!InitializeAcceptedCreateResidence( + canonical, + result, + beginInitialResidence, + isLocalPlayer)) + { + throw FailInitialResidenceRegistration( + canonical, + publishDeleted: false); + } try { PublishEntity(RuntimeEntityChange.Registered, canonical); @@ -446,10 +529,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable { EnsureNotDisposed(); ArgumentNullException.ThrowIfNull(canonical); - RuntimePlacementCancellationReceipt cancellation = + RuntimePlacementCancellationReceipt initialCancellation = + ForgetInitialCreateResidence(canonical); + RuntimePlacementCancellationReceipt ordinaryCancellation = Physics.SetPosition.Forget( canonical, releasePreparedMover: true); + RuntimePlacementCancellationReceipt cancellation = + PreferCancellation(initialCancellation, ordinaryCancellation); Physics.CollisionReports.Forget(canonical); Physics.RemoveSpatialProjection(canonical); Entities.SetRemoteMotion(canonical, null); @@ -540,10 +627,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable } Entities.RefreshSnapshot(canonical, accepted); + RuntimePlacementCancellationReceipt initialCancellation = + ForgetInitialCreateResidence(canonical); Entities.AdvancePositionAuthority(canonical); Physics.CollisionReports.LeaveWorld(canonical); - RuntimePlacementCancellationReceipt cancellation = + RuntimePlacementCancellationReceipt ordinaryCancellation = Physics.SetPosition.Forget(canonical); + RuntimePlacementCancellationReceipt cancellation = + PreferCancellation(initialCancellation, ordinaryCancellation); Entities.SuspendObjectClock(canonical); Entities.SetFullCell(canonical, 0u, 0u); Entities.ParentAttachments.EndChildProjection(update.Guid); @@ -631,8 +722,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable return false; } - RuntimePlacementCancellationReceipt cancellation = + RuntimePlacementCancellationReceipt initialCancellation = + ForgetInitialCreateResidence(canonical); + RuntimePlacementCancellationReceipt ordinaryCancellation = Physics.SetPosition.Forget(canonical); + RuntimePlacementCancellationReceipt cancellation = + PreferCancellation(initialCancellation, ordinaryCancellation); Physics.CollisionReports.LeaveWorld(canonical); Entities.SuspendObjectClock(canonical); Entities.SetFullCell(canonical, 0u, 0u); @@ -788,6 +883,25 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable out AcceptedPhysicsTimestamps timestamps) { EnsureNotDisposed(); + RuntimeInitialCreateResidenceLease priorInitialResidence = default; + bool hadPendingInitialResidence = + Entities.TryGetActive( + update.Guid, + out RuntimeEntityRecord pendingCanonical) + && InitialCreateResidences.TryGetTransaction( + pendingCanonical, + out priorInitialResidence); + if (hadPendingInitialResidence + && !InitialCreateResidences.CanEnqueueAcceptedPosition( + pendingCanonical, + priorInitialResidence, + update)) + { + disposition = PositionTimestampDisposition.Rejected; + accepted = default; + timestamps = default; + return false; + } bool hadCanonical = Entities.TryGetActive( update.Guid, out RuntimeEntityRecord beforeCanonical); @@ -825,9 +939,47 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable }; } - RuntimePlacementCancellationReceipt cancellation = acceptedPosition - ? Physics.SetPosition.Forget(canonical) - : default; + + // 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) + { + cancellation = Physics.SetPosition.Forget(canonical); + } Entities.RefreshSnapshot( canonical, snapshot, @@ -903,6 +1055,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable if (!Entities.IsCurrent(canonical)) return false; + RuntimePlacementCancellationReceipt cancellation = + ForgetInitialCreateResidence(canonical); Physics.CollisionReports.LeaveWorld(canonical); Entities.SuspendObjectClock(canonical); Entities.SetFullCell(canonical, 0u, 0u); @@ -911,7 +1065,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable canonical, () => acknowledgeProjection?.Invoke(canonical), RuntimeEntityChange.Withdrawn, - () => canonical.SpatialAuthorityVersion == spatialVersion); + () => canonical.SpatialAuthorityVersion == spatialVersion, + cancellation); } public bool CommitChildNoDraw( @@ -981,11 +1136,17 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable && active.Incarnation == delete.InstanceSequence && Entities.RemoveActive(active)) { + RuntimePlacementCancellationReceipt initialCancellation = + ForgetInitialCreateResidence(active); Physics.CollisionReports.Forget(active); - RuntimePlacementCancellationReceipt cancellation = + RuntimePlacementCancellationReceipt ordinaryCancellation = Physics.SetPosition.Forget( active, releasePreparedMover: true); + RuntimePlacementCancellationReceipt cancellation = + PreferCancellation( + initialCancellation, + ordinaryCancellation); retiredCanonical = active; Entities.RetainTeardown(active); Physics.SetPosition.PublishCancellation(cancellation); @@ -1058,6 +1219,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable _sessionClearInProgress = true; RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray(); + InitialCreateResidences.Clear(); Physics.CollisionReports.LeaveWorldBatch(active); Physics.ResetSessionPhysics(); Entities.BeginSessionClear(); @@ -1193,10 +1355,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable } Entities.RefreshSnapshot(canonical, accepted); + RuntimePlacementCancellationReceipt initialCancellation = + ForgetInitialCreateResidence(canonical); Entities.AdvancePositionAuthority(canonical); Physics.CollisionReports.LeaveWorld(canonical); - RuntimePlacementCancellationReceipt cancellation = + RuntimePlacementCancellationReceipt ordinaryCancellation = Physics.SetPosition.Forget(canonical); + RuntimePlacementCancellationReceipt cancellation = + PreferCancellation(initialCancellation, ordinaryCancellation); ulong positionVersion = canonical.PositionAuthorityVersion; ulong spatialVersion = canonical.SpatialAuthorityVersion; return AcknowledgeProjectionAndPublish( @@ -1251,6 +1417,108 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable private void EnsureNotDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); + internal bool TryGetInitialCreateResidence( + RuntimeEntityRecord canonical, + out RuntimeInitialCreateResidenceLease lease) + { + EnsureNotDisposed(); + return InitialCreateResidences.TryGetCurrent(canonical, out lease); + } + + internal RuntimeInitialCreateResidenceCompletionStatus + CompleteInitialCreateResidence( + RuntimeEntityRecord canonical, + in RuntimeInitialCreateResidenceToken token, + out RuntimeInitialCreateResidenceReceipt receipt) + { + EnsureNotDisposed(); + return InitialCreateResidences.Complete( + canonical, + token, + out receipt); + } + + internal bool AcknowledgeInitialCreateResidenceAdoption( + RuntimeEntityRecord canonical, + in RuntimeInitialCreateResidenceAdoptionToken token) + { + EnsureNotDisposed(); + return InitialCreateResidences.AcknowledgeAdoption( + canonical, + token); + } + + private RuntimePlacementCancellationReceipt ForgetInitialCreateResidence( + RuntimeEntityRecord canonical) + { + return InitialCreateResidences.Forget( + canonical, + out _, + out RuntimePlacementCancellationReceipt cancellation) + ? cancellation + : default; + } + + private static RuntimePlacementCancellationReceipt PreferCancellation( + in RuntimePlacementCancellationReceipt initial, + in RuntimePlacementCancellationReceipt ordinary) => + initial.IsValid ? initial : ordinary; + + private bool InitializeAcceptedCreateResidence( + RuntimeEntityRecord canonical, + in InboundCreateResult accepted, + bool beginInitialResidence, + bool isLocalPlayer) + { + if (!beginInitialResidence) + return true; + + // The explicit cutover path separates accepted wire authority from + // committed residence before any observer can hydrate it. Failure to + // own the exact route is fail-closed; the caller removes the canonical + // record before publishing Registered/Updated. + if (canonical.PositionAuthorityVersion == 0UL) + Entities.AdvancePositionAuthority(canonical); + if (canonical.FullCellId != 0u) + Entities.SetFullCell(canonical, 0u, 0u); + RuntimeInitialCreateResidenceLease lease = + InitialCreateResidences.Begin( + canonical, + accepted, + isLocalPlayer); + return lease.IsValid; + } + + private Exception FailInitialResidenceRegistration( + RuntimeEntityRecord canonical, + bool publishDeleted) + { + if (!Entities.RemoveActive(canonical)) + { + return new InvalidOperationException( + $"Initial residence for 0x{canonical.ServerGuid:X8} failed after its canonical incarnation was superseded."); + } + + Exception? failure = null; + if (publishDeleted) + { + try + { + PublishEntity(RuntimeEntityChange.Deleted, canonical); + } + catch (Exception error) + { + failure = error; + } + } + failure = Combine(failure, RetireCanonicalOnly(canonical)); + var cause = new InvalidOperationException( + $"Initial residence for 0x{canonical.ServerGuid:X8} could not acquire its exact Runtime placement lease."); + return failure is null + ? cause + : new AggregateException(cause, failure); + } + private static Exception? Combine( Exception? first, Exception? second) => diff --git a/src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs b/src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs new file mode 100644 index 00000000..f35fa9f5 --- /dev/null +++ b/src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs @@ -0,0 +1,743 @@ +using System.Collections.Immutable; +using System.Numerics; +using AcDream.Core.Net; +using AcDream.Core.Net.Messages; +using AcDream.Core.Physics; +using AcDream.Runtime.Physics; + +namespace AcDream.Runtime.Entities; + +internal readonly record struct RuntimeInitialCreateResidenceToken( + RuntimeEntityKey Entity, + ulong LeaseId, + ulong SessionLifetimeVersion, + ulong PositionAuthorityVersion, + ulong CreateIntegrationVersion, + ulong SourcePlacementCommitVersion) +{ + internal bool IsValid => Entity.LocalEntityId != 0u + && LeaseId != 0UL + && PositionAuthorityVersion != 0UL + && CreateIntegrationVersion != 0UL; +} + +/// +/// Exact, presentation-free initial CreateObject residence authority. The +/// accepted wire frame remains on the canonical record while FullCell stays +/// zero until the authored Runtime SetPosition operation commits. +/// +internal readonly record struct RuntimeInitialCreateResidenceLease( + RuntimeInitialCreateResidenceToken Token, + RuntimeAuthoritativePositionRoute Route, + RuntimeEntityPlacementToken Placement, + ImmutableArray Continuations) +{ + internal bool IsValid => Token.IsValid + && Route.Accepted + && Route.Authority.Entity == Token.Entity + && !Continuations.IsDefault + && HasValidContinuationChain() + && (!Route.PerformsSetPosition + || Placement.IsValid + && Placement.Entity == Token.Entity); + + private bool HasValidContinuationChain() + { + ushort previousTeleport = Route.Authority.AcceptedTeleportSequence; + for (int index = 0; index < Continuations.Length; index++) + { + RuntimeInitialCreateResidenceContinuation continuation = + Continuations[index]; + if (!continuation.IsValid + || continuation.Sequence != (ulong)index + 1UL + || continuation.InstanceSequence != Token.Entity.Incarnation + || continuation.PositionAuthorityVersion + != Token.PositionAuthorityVersion + || continuation.PreviousTeleportSequence + != previousTeleport) + { + return false; + } + previousTeleport = continuation.AcceptedTeleportSequence; + } + return true; + } +} + +/// +/// 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. +/// +internal readonly record struct RuntimeInitialCreateResidenceContinuation( + ulong Sequence, + RuntimePositionEntityKind EntityKind, + 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) +{ + internal bool IsValid => Sequence != 0UL + && EntityKind is RuntimePositionEntityKind.LocalPlayer + or RuntimePositionEntityKind.Remote + or RuntimePositionEntityKind.Projectile + && PositionAuthorityVersion != 0UL + && TimestampDisposition is PositionTimestampDisposition.Apply + or PositionTimestampDisposition.ForcePosition; +} + +internal readonly record struct RuntimeInitialCreateResidenceAdoptionToken( + RuntimeEntityKey Entity, + ulong LeaseId, + ulong AdoptionId, + ulong SessionLifetimeVersion, + ulong Revision) +{ + internal bool IsValid => Entity.LocalEntityId != 0u + && LeaseId != 0UL + && AdoptionId != 0UL + && Revision != 0UL; +} + +internal enum RuntimeInitialCreateResidenceCompletionStatus : byte +{ + Completed, + PendingPlacement, + RejectedToken, + RejectedAuthority, +} + +/// +/// Exact post-residence receipt. A local graphical or no-window host may run +/// the retail after-enter teleport suffix only when this receipt carries +/// . +/// +internal readonly record struct RuntimeInitialCreateResidenceReceipt( + RuntimeInitialCreateResidenceToken Token, + RuntimeTeleportHookPhase TeleportHookPhase, + RuntimePlacementProjectionToken Projection, + uint FullCellId, + ulong PlacementCommitVersion, + RuntimeInitialCreateResidenceAdoptionToken Adoption, + ImmutableArray Continuations); + +internal readonly record struct RuntimeInitialCreateResidenceOwnershipSnapshot( + int ActiveLeaseCount, + int PendingAdoptionCount, + ulong LastLeaseId) +{ + internal bool IsConverged => ActiveLeaseCount == 0 + && PendingAdoptionCount == 0; +} + +/// +/// Owns only initial CreateObject residence leases. DAT lookup, body creation, +/// and presentation stay outside this owner; their immutable preparation is +/// submitted through the lease's canonical Runtime SetPosition token. +/// +internal sealed class RuntimeInitialCreateResidenceState +{ + private sealed class Entry + { + internal required RuntimeEntityRecord Record { get; init; } + internal required RuntimeInitialCreateResidenceLease Lease { get; set; } + } + + private sealed class CompletedEntry + { + internal required RuntimeEntityRecord Record { get; init; } + internal required RuntimeInitialCreateResidenceLease Lease { get; set; } + internal required RuntimeInitialCreateResidenceReceipt Receipt { get; set; } + } + + private readonly RuntimeEntityDirectory _entities; + private readonly RuntimeSetPositionState _setPosition; + private readonly Dictionary _entries = []; + private readonly Dictionary _completed = []; + private Func? _generation; + private ulong _nextLeaseId; + + internal RuntimeInitialCreateResidenceState( + RuntimeEntityDirectory entities, + RuntimeSetPositionState setPosition) + { + _entities = entities ?? throw new ArgumentNullException(nameof(entities)); + _setPosition = setPosition + ?? throw new ArgumentNullException(nameof(setPosition)); + } + + internal void BindGeneration(Func generation) + { + ArgumentNullException.ThrowIfNull(generation); + if (_generation is not null) + { + throw new InvalidOperationException( + "The initial Create residence generation source is already bound."); + } + _generation = generation; + } + + internal bool CanAcceptCreate(WorldSession.EntitySpawn incoming) + { + bool parented = (incoming.ParentGuid + ?? incoming.Physics?.Parent?.Guid) + is not null and not 0u; + bool topLevel = !parented + && incoming.Position is { LandblockId: not 0u }; + return CurrentGeneration().Value != 0UL + && _nextLeaseId != ulong.MaxValue + && (!topLevel + || _setPosition.CanBeginAuthoredPlacementSequence + && RuntimeAuthoritativePositionRouteClassifier + .IsValidCreateWirePosition( + incoming.Position!.Value)); + } + + internal RuntimeInitialCreateResidenceLease Begin( + RuntimeEntityRecord record, + in InboundCreateResult accepted, + bool isLocalPlayer) + { + ArgumentNullException.ThrowIfNull(record); + if (!_entities.IsCurrent(record) + || record.Key is not { } key + || record.FullCellId != 0u + || accepted.Snapshot.Guid != record.ServerGuid + || accepted.Snapshot.InstanceSequence != record.Incarnation) + { + return default; + } + + RuntimeGenerationToken generation = CurrentGeneration(); + RuntimePositionEntityKind entityKind = isLocalPlayer + ? RuntimePositionEntityKind.LocalPlayer + : (record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0 + ? RuntimePositionEntityKind.Projectile + : RuntimePositionEntityKind.Remote; + WorldSession.EntitySpawn snapshot = accepted.Snapshot; + // PhysicsDesc parent ownership precedes its optional position frame in + // retail set_description. A parented Create is not a top-level world + // admission merely because ACE also supplied a position payload. + RuntimeCreateResidenceKind residence = + (snapshot.ParentGuid ?? snapshot.Physics?.Parent?.Guid) + is not null and not 0u + ? RuntimeCreateResidenceKind.Parented + : snapshot.Position is { LandblockId: not 0u } + ? RuntimeCreateResidenceKind.TopLevel + : RuntimeCreateResidenceKind.PickedUp; + var authority = new RuntimeAuthoritativePositionAuthority( + generation, + key, + record.PositionAuthorityVersion, + snapshot.PositionSequence, + accepted.Timestamps.Teleport, + accepted.Timestamps.Teleport, + PositionTimestampDisposition.Apply); + RuntimeAuthoritativePositionRoute route = + RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate( + new RuntimeCreatePositionRouteRequest( + authority, + entityKind, + residence, + snapshot.Position, + new RuntimePositionPlacementFacts( + record.FinalPhysicsState, + HasAuthoredMoverShape: snapshot.SetupTableId is not null))); + return Own(record, route); + } + + internal RuntimeInitialCreateResidenceLease EnqueueAcceptedPosition( + 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) + { + ArgumentNullException.ThrowIfNull(record); + if (!_entities.IsCurrent(record) + || record.Key is not { } key + || update.Guid != record.ServerGuid + || accepted.Guid != record.ServerGuid + || disposition is PositionTimestampDisposition.Rejected) + { + return default; + } + + 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; + } + + internal bool CanEnqueueAcceptedPosition( + RuntimeEntityRecord record, + in RuntimeInitialCreateResidenceLease prior, + in WorldSession.EntityPositionUpdate update) + { + ArgumentNullException.ThrowIfNull(record); + if (record.Key is not { } key + || update.Guid != record.ServerGuid + || !RuntimeAuthoritativePositionRouteClassifier + .IsValidCreateWirePosition(update.Position)) + { + return false; + } + if (_entries.TryGetValue(key, out Entry? entry)) + { + return ReferenceEquals(entry.Record, record) + && entry.Lease.Token == prior.Token + && IsCurrent(entry) + && entry.Lease.Continuations.Length < int.MaxValue; + } + return _completed.TryGetValue(key, out CompletedEntry? completed) + && ReferenceEquals(completed.Record, record) + && completed.Lease.Token == prior.Token + && IsCompletedCurrent(completed) + && completed.Lease.Continuations.Length < int.MaxValue + && completed.Receipt.Adoption.Revision < ulong.MaxValue; + } + + internal bool TryGetTransaction( + RuntimeEntityRecord record, + out RuntimeInitialCreateResidenceLease lease) + { + if (TryGetCurrent(record, out lease)) + return true; + if (record.Key is { } key + && _completed.TryGetValue(key, out CompletedEntry? completed) + && ReferenceEquals(completed.Record, record)) + { + if (IsCompletedCurrent(completed)) + { + lease = completed.Lease; + return true; + } + Retire(completed); + } + lease = default; + return false; + } + + private RuntimeInitialCreateResidenceLease Own( + RuntimeEntityRecord record, + in RuntimeAuthoritativePositionRoute route) + { + RuntimeEntityKey key = record.Key!.Value; + if (_entries.ContainsKey(key) + || _nextLeaseId == ulong.MaxValue) + { + return default; + } + ulong leaseId = _nextLeaseId + 1UL; + + RuntimeEntityPlacementToken placement = default; + if (route.PerformsSetPosition) + { + placement = _setPosition.TryBeginExclusiveAuthoredPlacement( + record, + record.PositionAuthorityVersion, + route.OperationKind); + if (!placement.IsValid) + return default; + if (!_setPosition.WatchPlacementCompletion(placement)) + { + _ = _setPosition.ForgetExactPlacement(placement); + return default; + } + } + else if (!route.Accepted) + { + return default; + } + + var token = new RuntimeInitialCreateResidenceToken( + key, + leaseId, + _entities.SessionLifetimeVersion, + record.PositionAuthorityVersion, + record.CreateIntegrationVersion, + record.PlacementCommitVersion); + var lease = new RuntimeInitialCreateResidenceLease( + token, + route, + placement, + ImmutableArray.Empty); + _entries.Add(key, new Entry + { + Record = record, + Lease = lease, + }); + _nextLeaseId = leaseId; + return lease; + } + + internal bool TryGetCurrent( + RuntimeEntityRecord record, + out RuntimeInitialCreateResidenceLease lease) + { + ArgumentNullException.ThrowIfNull(record); + if (record.Key is { } key + && _entries.TryGetValue(key, out Entry? entry) + && ReferenceEquals(entry.Record, record)) + { + if (IsCurrent(entry)) + { + lease = entry.Lease; + return true; + } + Retire(entry); + } + lease = default; + return false; + } + + internal RuntimeInitialCreateResidenceCompletionStatus Complete( + RuntimeEntityRecord record, + in RuntimeInitialCreateResidenceToken token, + out RuntimeInitialCreateResidenceReceipt receipt) + { + ArgumentNullException.ThrowIfNull(record); + receipt = default; + if (token.IsValid + && _completed.TryGetValue(token.Entity, out CompletedEntry? completed) + && completed.Receipt.Token == token + && ReferenceEquals(completed.Record, record)) + { + if (IsCompletedCurrent(completed)) + { + receipt = completed.Receipt; + return RuntimeInitialCreateResidenceCompletionStatus.Completed; + } + Retire(completed); + return RuntimeInitialCreateResidenceCompletionStatus + .RejectedAuthority; + } + if (!token.IsValid + || !_entries.TryGetValue(token.Entity, out Entry? entry) + || entry.Lease.Token != token + || !ReferenceEquals(entry.Record, record)) + { + return RuntimeInitialCreateResidenceCompletionStatus.RejectedToken; + } + if (!IsCurrent(entry)) + { + Retire(entry); + return RuntimeInitialCreateResidenceCompletionStatus + .RejectedAuthority; + } + + RuntimeInitialCreateResidenceLease lease = entry.Lease; + RuntimePlacementProjectionToken projection = default; + if (lease.Route.PerformsSetPosition) + { + if (_setPosition.IsPlacementCurrent(lease.Placement)) + { + return RuntimeInitialCreateResidenceCompletionStatus + .PendingPlacement; + } + if (!_setPosition.TryPeekAcknowledgedPlacement( + lease.Placement, + out projection) + || projection.Entity != token.Entity + || projection.PositionAuthorityVersion + != token.PositionAuthorityVersion + || projection.SessionLifetimeVersion + != token.SessionLifetimeVersion + || projection.ExactCellId == 0u + || projection.ExactCellId != record.FullCellId + || projection.PlacementCommitVersion + <= token.SourcePlacementCommitVersion + || projection.PlacementCommitVersion + != record.PlacementCommitVersion) + { + Retire(entry); + return RuntimeInitialCreateResidenceCompletionStatus + .RejectedAuthority; + } + } + else if (record.FullCellId != 0u) + { + Retire(entry); + return RuntimeInitialCreateResidenceCompletionStatus + .RejectedAuthority; + } + + var adoption = new RuntimeInitialCreateResidenceAdoptionToken( + token.Entity, + token.LeaseId, + token.LeaseId, + token.SessionLifetimeVersion, + Revision: 1UL); + receipt = new RuntimeInitialCreateResidenceReceipt( + token, + lease.Route.TeleportHookPhase, + projection, + record.FullCellId, + record.PlacementCommitVersion, + adoption, + lease.Continuations); + _entries.Remove(token.Entity); + _completed.Add(token.Entity, new CompletedEntry + { + Record = record, + Lease = lease, + Receipt = receipt, + }); + return RuntimeInitialCreateResidenceCompletionStatus.Completed; + } + + internal bool AcknowledgeAdoption( + RuntimeEntityRecord record, + in RuntimeInitialCreateResidenceAdoptionToken token) + { + ArgumentNullException.ThrowIfNull(record); + if (!token.IsValid) + return false; + if (!_completed.TryGetValue(token.Entity, out CompletedEntry? current) + || !ReferenceEquals(current.Record, record) + || current.Receipt.Adoption != token) + { + return false; + } + if (!IsCompletedCurrent(current)) + { + Retire(current); + return false; + } + if (current.Lease.Route.PerformsSetPosition + && !_setPosition.ConsumeAcknowledgedPlacement( + current.Lease.Placement, + current.Receipt.Projection)) + { + return false; + } + return _completed.Remove(token.Entity); + } + + internal bool Forget( + RuntimeEntityRecord record, + out RuntimeInitialCreateResidenceLease lease, + out RuntimePlacementCancellationReceipt cancellation) + { + ArgumentNullException.ThrowIfNull(record); + cancellation = default; + if (record.Key is { } key + && _entries.TryGetValue(key, out Entry? entry) + && ReferenceEquals(entry.Record, record) + && _entries.Remove(key)) + { + lease = entry.Lease; + cancellation = _setPosition.ForgetExactPlacement( + lease.Placement); + return true; + } + if (record.Key is { } completedKey + && _completed.TryGetValue( + completedKey, + out CompletedEntry? completed) + && ReferenceEquals(completed.Record, record) + && _completed.Remove(completedKey)) + { + lease = completed.Lease; + cancellation = _setPosition.ForgetExactPlacement( + lease.Placement); + return true; + } + lease = default; + return false; + } + + internal void Clear() + { + Entry[] active = _entries.Values.ToArray(); + CompletedEntry[] completed = _completed.Values.ToArray(); + var cancellations = new RuntimePlacementCancellationReceipt[ + active.Length + completed.Length]; + _entries.Clear(); + _completed.Clear(); + int cancellationCount = 0; + foreach (Entry entry in active) + { + RuntimePlacementCancellationReceipt cancellation = + _setPosition.ForgetExactPlacement( + entry.Lease.Placement); + if (cancellation.IsValid) + cancellations[cancellationCount++] = cancellation; + } + foreach (CompletedEntry entry in completed) + { + RuntimePlacementCancellationReceipt cancellation = + _setPosition.ForgetExactPlacement( + entry.Lease.Placement); + if (cancellation.IsValid) + cancellations[cancellationCount++] = cancellation; + } + for (int index = 0; index < cancellationCount; index++) + { + _setPosition.PublishCancellation(cancellations[index]); + } + } + + internal RuntimeInitialCreateResidenceOwnershipSnapshot CaptureOwnership() => + new(_entries.Count, _completed.Count, _nextLeaseId); + + private bool IsCurrent(Entry entry) + { + RuntimeInitialCreateResidenceToken token = entry.Lease.Token; + bool placementCurrent = !entry.Lease.Route.PerformsSetPosition + || _setPosition.IsPlacementCompletionTracked( + entry.Lease.Placement); + return placementCurrent + && _entities.IsCurrent(entry.Record) + && entry.Record.Key == token.Entity + && _entities.SessionLifetimeVersion + == token.SessionLifetimeVersion + && entry.Record.PositionAuthorityVersion + == token.PositionAuthorityVersion + && entry.Record.CreateIntegrationVersion + == token.CreateIntegrationVersion + && entry.Lease.Route.Authority.Generation + == CurrentGeneration(); + } + + private RuntimeGenerationToken CurrentGeneration() + { + return _generation?.Invoke() ?? default; + } + + private bool IsCompletedCurrent(CompletedEntry entry) + { + RuntimeInitialCreateResidenceReceipt receipt = entry.Receipt; + return _entities.IsCurrent(entry.Record) + && entry.Record.Key == receipt.Token.Entity + && _entities.SessionLifetimeVersion + == receipt.Token.SessionLifetimeVersion + && entry.Record.PositionAuthorityVersion + == receipt.Token.PositionAuthorityVersion + && entry.Record.CreateIntegrationVersion + == receipt.Token.CreateIntegrationVersion + && entry.Record.FullCellId == receipt.FullCellId + && entry.Record.PlacementCommitVersion + == receipt.PlacementCommitVersion + && entry.Lease.Route.Authority.Generation + == CurrentGeneration() + && receipt.Token.SessionLifetimeVersion + == receipt.Adoption.SessionLifetimeVersion + && receipt.Token.LeaseId == receipt.Adoption.LeaseId + && (!entry.Lease.Route.PerformsSetPosition + || _setPosition.IsPlacementCompletionTracked( + entry.Lease.Placement)); + } + + private void Retire(Entry entry) + { + _entries.Remove(entry.Lease.Token.Entity); + RuntimePlacementCancellationReceipt cancellation = + _setPosition.ForgetExactPlacement(entry.Lease.Placement); + _setPosition.PublishCancellation(cancellation); + } + + private void Retire(CompletedEntry entry) + { + _completed.Remove(entry.Receipt.Token.Entity); + RuntimePlacementCancellationReceipt cancellation = + _setPosition.ForgetExactPlacement(entry.Lease.Placement); + _setPosition.PublishCancellation(cancellation); + } +} diff --git a/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs b/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs index ab849c48..28c70f8e 100644 --- a/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs +++ b/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs @@ -180,6 +180,9 @@ internal static class RuntimeAuthoritativePositionRouteClassifier | PhysicsSetPositionFlags.Slide | PhysicsSetPositionFlags.SendPositionEvent; + internal static bool IsValidCreateWirePosition( + in CreateObject.ServerPosition position) => ValidPosition(position); + internal static RuntimeAuthoritativePositionRoute ClassifyCreate( in RuntimeCreatePositionRouteRequest request) { @@ -205,7 +208,10 @@ internal static class RuntimeAuthoritativePositionRouteClassifier 0u, UnparentBeforeRouting: false, ApplyPlacementFrameBeforeRouting: false, - LeaveWorld: true, + // A fresh cellless CreateObject has never entered world. + // Parent composition and later pickup events own their own + // callbacks; initial residence invents no withdrawal edge. + LeaveWorld: false, TeleportHookPhase: RuntimeTeleportHookPhase.None, StopInterpolating: false, ConstrainAfterRouting: false, @@ -294,7 +300,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier { return new RuntimeAuthoritativePositionRoute( request.Authority, - RuntimeAuthoritativePositionDisposition.SetPosition, + RuntimeAuthoritativePositionDisposition.SetPositionSimple, operation, AuthoritativeTeleportFlags, placement, diff --git a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs index 86aae1d4..4f6dc5bb 100644 --- a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs +++ b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs @@ -250,6 +250,8 @@ internal readonly record struct RuntimeSetPositionOwnershipSnapshot( int UnboundDeferredCellOrderCount, int PreparedMoverCount, int MoverPreparationAuthorityCount, + int PlacementCompletionWatchCount, + int AcknowledgedPlacementCompletionCount, int CollisionPrefixQuiescenceCount, int PendingQuiescenceProjectionCount) { @@ -276,6 +278,8 @@ internal readonly record struct RuntimeSetPositionOwnershipSnapshot( && UnboundDeferredCellOrderCount == 0 && PreparedMoverCount == 0 && MoverPreparationAuthorityCount == 0 + && PlacementCompletionWatchCount == 0 + && AcknowledgedPlacementCompletionCount == 0 && CollisionPrefixQuiescenceCount == 0 && PendingQuiescenceProjectionCount == 0; } @@ -414,6 +418,10 @@ internal sealed class RuntimeSetPositionState : IDisposable _preparedMovers = []; private readonly Dictionary _moverPreparationAuthorities = []; + private readonly HashSet + _placementCompletionWatches = []; + private readonly Dictionary _acknowledgedPlacementCompletions = []; private readonly Dictionary _collisionPrefixQuiescence = []; private readonly LinkedList _expiredLostCells = []; @@ -477,11 +485,15 @@ internal sealed class RuntimeSetPositionState : IDisposable _unboundDeferredCellOrder.Count, _preparedMovers.Count, _moverPreparationAuthorities.Count, + _placementCompletionWatches.Count, + _acknowledgedPlacementCompletions.Count, _collisionPrefixQuiescence.Count, pendingQuiescenceProjections); } internal int PendingProjectionCount => _pendingProjection.Count; + internal bool CanBeginAuthoredPlacementSequence => + _nextOperationId != ulong.MaxValue; internal bool IsCollisionPrefixQuiescing(uint landblockId) => _collisionPrefixQuiescence.ContainsKey( @@ -812,6 +824,111 @@ internal sealed class RuntimeSetPositionState : IDisposable portal, captureMoverPreparationAuthority: true); + internal bool IsPlacementCurrent( + in RuntimeEntityPlacementToken token) + { + EnsureNotDisposed(); + return token.IsValid + && _operations.TryGetValue(token.Entity, out Operation? operation) + && operation.Token == token + && IsCurrent(operation); + } + + /// + /// Reserves the exact acknowledgement edge for a higher-level Runtime + /// transaction. Ordinary SetPosition operations retain no completion + /// history; only explicitly watched tokens survive operation retirement. + /// + internal bool WatchPlacementCompletion( + in RuntimeEntityPlacementToken token) + { + EnsureNotDisposed(); + return IsPlacementCurrent(token) + && _placementCompletionWatches.Add(token); + } + + internal bool IsPlacementCompletionTracked( + in RuntimeEntityPlacementToken token) + { + EnsureNotDisposed(); + return token.IsValid + && (IsPlacementCurrent(token) + && _placementCompletionWatches.Contains(token) + || _acknowledgedPlacementCompletions.ContainsKey(token)); + } + + internal bool TryPeekAcknowledgedPlacement( + in RuntimeEntityPlacementToken token, + out RuntimePlacementProjectionToken projection) + { + EnsureNotDisposed(); + if (token.IsValid + && _acknowledgedPlacementCompletions.TryGetValue( + token, + out projection)) + { + return true; + } + projection = default; + return false; + } + + internal bool ConsumeAcknowledgedPlacement( + in RuntimeEntityPlacementToken token, + in RuntimePlacementProjectionToken expected) + { + EnsureNotDisposed(); + return token.IsValid + && _acknowledgedPlacementCompletions.TryGetValue( + token, + out RuntimePlacementProjectionToken current) + && current == expected + && _acknowledgedPlacementCompletions.Remove(token); + } + + internal void ForgetPlacementCompletion( + in RuntimeEntityPlacementToken token) + { + EnsureNotDisposed(); + ForgetPlacementCompletionCore(token); + } + + internal RuntimePlacementCancellationReceipt ForgetExactPlacement( + in RuntimeEntityPlacementToken token) + { + EnsureNotDisposed(); + ForgetPlacementCompletionCore(token); + if (!token.IsValid + || !_operations.TryGetValue(token.Entity, out Operation? operation) + || operation.Token != token) + { + return default; + } + return CancelCore(operation); + } + + internal RuntimeEntityPlacementToken TryBeginExclusiveAuthoredPlacement( + RuntimeEntityRecord record, + ulong expectedPositionAuthorityVersion, + RuntimeSetPositionOperationKind kind, + RuntimePortalPlacementAuthority portal = default) + { + EnsureNotDisposed(); + ArgumentNullException.ThrowIfNull(record); + if (record.Key is not { } key + || _operations.ContainsKey(key) + || HasRetainedCompletion(key)) + { + return default; + } + return BeginAcceptedPlacementCore( + record, + expectedPositionAuthorityVersion, + kind, + portal, + captureMoverPreparationAuthority: true); + } + internal void PrepareDormantLocalActivationOwnership( RuntimeEntityRecord record, PhysicsBody body, @@ -853,6 +970,7 @@ internal sealed class RuntimeSetPositionState : IDisposable record.Snapshot.Physics?.Position ?? record.Snapshot.Position; if (record.Key is not { } key || !_entities.IsCurrent(record) + || HasRetainedCompletion(key) || record.PositionAuthorityVersion != expectedPositionAuthorityVersion || (captureMoverPreparationAuthority @@ -947,6 +1065,17 @@ internal sealed class RuntimeSetPositionState : IDisposable return IsCurrent(replacement) ? token : default; } + private bool HasRetainedCompletion(RuntimeEntityKey key) + { + foreach (RuntimeEntityPlacementToken token + in _acknowledgedPlacementCompletions.Keys) + { + if (token.Entity == key) + return true; + } + return false; + } + internal RuntimeSetPositionMoverPreparationStatus PrepareMover( in RuntimeEntityPlacementToken token, in RuntimeSetPositionMoverPreparation preparation, @@ -2206,6 +2335,12 @@ internal sealed class RuntimeSetPositionState : IDisposable operation.ProjectionSequence = 0UL; if (pending.Kind is RuntimePlacementProjectionKind.Place) { + if (_placementCompletionWatches.Remove(operation.Token)) + { + _acknowledgedPlacementCompletions.Add( + operation.Token, + pending.Token); + } operation.Stage = RuntimeEntityPlacementStage.AwaitingCommitAcknowledgement; _moverPreparationAuthorities.Remove(operation.Key); return _operations.Remove(operation.Key); @@ -3041,6 +3176,8 @@ internal sealed class RuntimeSetPositionState : IDisposable _lostDeadlineNodeIndex.Clear(); _preparedMovers.Clear(); _moverPreparationAuthorities.Clear(); + _placementCompletionWatches.Clear(); + _acknowledgedPlacementCompletions.Clear(); _collisionPrefixQuiescence.Clear(); _pendingProjection.Clear(); _expiredLostCells.Clear(); @@ -3859,6 +3996,7 @@ internal sealed class RuntimeSetPositionState : IDisposable CancelExactLostKey(key); if (!_operations.Remove(key, out Operation? operation)) return false; + ForgetPlacementCompletionCore(operation.Token); _moverPreparationAuthorities.Remove(key); UnindexDeferred(operation); if (!preserveLostFamily && cancelLostFamily) @@ -3929,6 +4067,15 @@ internal sealed class RuntimeSetPositionState : IDisposable : default; } + private void ForgetPlacementCompletionCore( + in RuntimeEntityPlacementToken token) + { + if (!token.IsValid) + return; + _placementCompletionWatches.Remove(token); + _acknowledgedPlacementCompletions.Remove(token); + } + private void IndexDeferred(Operation operation) { if (operation.ExactCellId == 0u diff --git a/tests/AcDream.Core.Tests/Physics/MotionSequenceGateTests.cs b/tests/AcDream.Core.Tests/Physics/MotionSequenceGateTests.cs index 8941dcf3..e8562f3d 100644 --- a/tests/AcDream.Core.Tests/Physics/MotionSequenceGateTests.cs +++ b/tests/AcDream.Core.Tests/Physics/MotionSequenceGateTests.cs @@ -118,6 +118,35 @@ public class PhysicsTimestampGateTests Assert.True(gate.TryAcceptMovementEvent(4, 0x9011, 2)); } + [Fact] + public void PreviewCreateObject_MatchesSeedDispositionWithoutMutation() + { + var gate = new PhysicsTimestampGate(); + + Assert.Equal(CreateObjectTimestampDisposition.InitialGeneration, + gate.PreviewCreateObject(instance: 7)); + Assert.Equal(CreateObjectTimestampDisposition.InitialGeneration, + gate.SeedForCreateObject(10, 20, 30, 40, 50, 60, 70, 80, 7)); + + Assert.Equal(CreateObjectTimestampDisposition.ExistingGeneration, + gate.PreviewCreateObject(instance: 7)); + Assert.Equal(CreateObjectTimestampDisposition.NewGeneration, + gate.PreviewCreateObject(instance: 8)); + Assert.Equal(CreateObjectTimestampDisposition.StaleGeneration, + gate.PreviewCreateObject(instance: 6)); + + // Preview is observational: all timestamp channels still compare + // against the original instance-seven seed afterward. + Assert.True(gate.TryAcceptMovementEvent( + instance: 7, + movement: 21, + serverControlledMove: 60)); + Assert.False(gate.TryAcceptMovementEvent( + instance: 8, + movement: 22, + serverControlledMove: 60)); + } + [Fact] public void SameGenerationCreateObject_MixedChannelsAreAcceptedIndependently() { diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs new file mode 100644 index 00000000..1ebebabb --- /dev/null +++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs @@ -0,0 +1,1792 @@ +using System.Collections.Immutable; +using System.Numerics; +using AcDream.Core.Net; +using AcDream.Core.Net.Messages; +using AcDream.Core.Physics; +using AcDream.Runtime.Entities; +using AcDream.Runtime.Physics; + +namespace AcDream.Runtime.Tests.Entities; + +public sealed class RuntimeInitialCreateResidenceStateTests +{ + private const uint Landblock = 0xA9B40000u; + private const uint Cell = Landblock | 0x0001u; + + [Theory] + [InlineData(true, false, + RuntimeSetPositionOperationKind.InitialLogin, + RuntimeTeleportHookPhase.AfterEnterWorld)] + [InlineData(false, false, + RuntimeSetPositionOperationKind.RemoteAuthoritative, + RuntimeTeleportHookPhase.None)] + [InlineData(false, true, + RuntimeSetPositionOperationKind.ProjectileAuthoritative, + RuntimeTeleportHookPhase.None)] + internal void TopLevelRegistrationOwnsExactCelllessLeaseBeforeObservers( + bool isLocalPlayer, + bool missile, + RuntimeSetPositionOperationKind expectedOperation, + RuntimeTeleportHookPhase expectedHook) + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 7UL); + RuntimeInitialCreateResidenceLease observed = default; + uint observedCell = uint.MaxValue; + using IDisposable subscription = lifetime.Events.Subscribe( + new EntityObserver(delta => + { + if (delta.Change is not RuntimeEntityChange.Registered) + return; + Assert.True(lifetime.Entities.TryGetByLocalId( + delta.Entity.Identity.LocalEntityId, + out RuntimeEntityRecord record)); + observedCell = record.FullCellId; + Assert.True(lifetime.TryGetInitialCreateResidence( + record, + out observed)); + })); + + RuntimeEntityRegistrationResult registration = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003001u, 1, missile: missile), + isLocalPlayer); + RuntimeEntityRecord canonical = registration.Canonical!; + + Assert.Equal(0u, observedCell); + Assert.Equal(0u, canonical.FullCellId); + Assert.Equal(Cell, canonical.Snapshot.Position!.Value.LandblockId); + Assert.True(observed.IsValid); + Assert.Equal(observed.Token.Entity, observed.Route.Authority.Entity); + Assert.Equal(expectedOperation, observed.Route.OperationKind); + Assert.Equal(expectedHook, observed.Route.TeleportHookPhase); + Assert.Equal(0x11u, (uint)observed.Route.SetPositionFlags); + Assert.True(observed.Placement.IsValid); + Assert.Equal(1, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void FreshParentedAndPickedCreateRemainCelllessWithoutWithdrawal( + bool parented) + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 8UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + 0x70003002u, + 1, + includePosition: false, + parentGuid: parented ? 0x70004000u : null), + isLocalPlayer: false) + .Canonical!; + + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + Assert.Equal(0u, canonical.FullCellId); + Assert.False(lease.Placement.IsValid); + Assert.Equal(RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, + lease.Route.Disposition); + Assert.False(lease.Route.LeaveWorld); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out RuntimeInitialCreateResidenceReceipt receipt)); + Assert.Equal(0u, receipt.FullCellId); + Assert.Equal(RuntimeTeleportHookPhase.None, + receipt.TeleportHookPhase); + Assert.True(lifetime.AcknowledgeInitialCreateResidenceAdoption( + canonical, + receipt.Adoption)); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership() + .ActiveOperationCount); + } + + [Fact] + public void MissingCellDefersExactLeaseWithoutCommittingWireResidence() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 9UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003003u, 1, setupId: null), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + AttachDormantBody(lifetime, canonical); + RuntimeSetPositionCommand command = Prepare( + lifetime, + lease, + RuntimeSetPositionMoverSetup.ResolvedAbsent); + + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition + .SubmitPreparedPlacement(lease.Placement, command); + + Assert.Equal(RuntimeSetPositionStatus.DeferredCell, outcome.Status); + Assert.Equal(0u, canonical.FullCellId); + Assert.Equal(Cell, canonical.Snapshot.Position!.Value.LandblockId); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.PendingPlacement, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out _)); + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease retained)); + Assert.Equal(lease, retained); + } + + [Theory] + [InlineData(true, RuntimeTeleportHookPhase.AfterEnterWorld)] + [InlineData(false, RuntimeTeleportHookPhase.None)] + internal void CommittedAndAcknowledgedCreateYieldsExactEnterWorldReceipt( + bool isLocalPlayer, + RuntimeTeleportHookPhase expectedHook) + { + var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; + engine.AddLandblock( + Landblock, + new TerrainSurface(new byte[81], new float[256]), + Array.Empty(), + Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + Bind(lifetime, 10UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003004u, 1, setupId: null), + isLocalPlayer) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + AttachDormantBody(lifetime, canonical); + RuntimeSetPositionCommand command = Prepare( + lifetime, + lease, + RuntimeSetPositionMoverSetup.ResolvedAbsent); + + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition + .SubmitPreparedPlacement(lease.Placement, command); + + Assert.Equal( + RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, + outcome.Status); + Assert.Equal(Cell, canonical.FullCellId); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.PendingPlacement, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out _)); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out RuntimeInitialCreateResidenceReceipt receipt)); + Assert.Equal(expectedHook, receipt.TeleportHookPhase); + Assert.Equal(Cell, receipt.FullCellId); + Assert.Equal(canonical.PlacementCommitVersion, + receipt.PlacementCommitVersion); + Assert.True(receipt.Adoption.IsValid); + Assert.Empty(receipt.Continuations); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out RuntimeInitialCreateResidenceReceipt repeated)); + Assert.Equal(receipt, repeated); + Assert.False(lifetime.Physics.SetPosition + .TryBeginExclusiveAuthoredPlacement( + canonical, + canonical.PositionAuthorityVersion, + RuntimeSetPositionOperationKind.RemoteAuthoritative) + .IsValid); + Assert.True(lifetime.AcknowledgeInitialCreateResidenceAdoption( + canonical, + receipt.Adoption)); + RuntimeEntityPlacementToken afterAdoption = lifetime.Physics.SetPosition + .TryBeginExclusiveAuthoredPlacement( + canonical, + canonical.PositionAuthorityVersion, + RuntimeSetPositionOperationKind.RemoteAuthoritative); + Assert.True(afterAdoption.IsValid); + _ = lifetime.Physics.SetPosition.ForgetExactPlacement(afterAdoption); + Assert.False(lifetime.TryGetInitialCreateResidence( + canonical, + out _)); + } + + [Fact] + public void GenerationReplacementForgetsOldLeaseAndOwnsOnlySuccessor() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 11UL); + RuntimeEntityRecord first = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003005u, 1), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + first, + out RuntimeInitialCreateResidenceLease oldLease)); + + RuntimeEntityRecord second = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003005u, 2), + isLocalPlayer: false) + .Canonical!; + + Assert.False(lifetime.TryGetInitialCreateResidence(first, out _)); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.RejectedToken, + lifetime.CompleteInitialCreateResidence( + first, + oldLease.Token, + out _)); + Assert.True(lifetime.TryGetInitialCreateResidence( + second, + out RuntimeInitialCreateResidenceLease successor)); + Assert.NotEqual(oldLease.Token, successor.Token); + Assert.Equal(1, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + } + + [Fact] + public void RegistrationObserverReentryCannotResurrectOuterLease() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 12UL); + RuntimeEntityRegistrationResult nested = default; + bool reentered = false; + using IDisposable subscription = lifetime.Events.Subscribe( + new EntityObserver(delta => + { + if (reentered + || delta.Change is not RuntimeEntityChange.Registered) + return; + reentered = true; + nested = lifetime.RegisterEntityWithInitialResidence( + Spawn(0x70003006u, 2), + isLocalPlayer: false); + })); + + RuntimeEntityRegistrationResult outer = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003006u, 1), + isLocalPlayer: false); + + Assert.Equal(CreateObjectTimestampDisposition.StaleGeneration, + outer.Inbound.Disposition); + RuntimeEntityRecord current = nested.Canonical!; + Assert.True(lifetime.Entities.IsCurrent(current)); + Assert.Equal((ushort)2, current.Incarnation); + Assert.True(lifetime.TryGetInitialCreateResidence( + current, + out RuntimeInitialCreateResidenceLease lease)); + Assert.Equal(current.Key, lease.Token.Entity); + Assert.Equal(1, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + } + + [Fact] + public void DeleteResetAndDisposeConvergeEveryInitialResidenceOwner() + { + var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 13UL); + RuntimeEntityRecord deleted = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003007u, 1), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryAcceptDelete( + new DeleteObject.Parsed(deleted.ServerGuid, deleted.Incarnation), + isLocalPlayer: false, + removeRetainedObject: true, + out RuntimeEntityDeleteAcceptance acceptance)); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + lifetime.CompleteAcceptedDelete(acceptance); + Assert.Null(lifetime.RetireCanonicalOnly(deleted)); + + _ = lifetime.RegisterEntityWithInitialResidence( + Spawn(0x70003008u, 1), + isLocalPlayer: false); + IReadOnlyList retirements = + lifetime.BeginSessionClear(); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + foreach (RuntimeEntityRecord record in retirements) + lifetime.CompleteSessionEntityRetirement(record); + Assert.True(lifetime.CompleteSessionClearIfConverged()); + + lifetime.Dispose(); + Assert.True(lifetime.CaptureOwnership().IsConverged); + } + + [Fact] + public void ResetCancelsCompletedUnadoptedContinuationBatch() + { + var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; + engine.AddLandblock( + Landblock, + new TerrainSurface(new byte[81], new float[256]), + Array.Empty(), + Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + Bind(lifetime, 32UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x7000301Eu, 1, setupId: null), + isLocalPlayer: true) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + AttachDormantBody(lifetime, canonical); + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition + .SubmitPreparedPlacement( + lease.Placement, + Prepare( + lifetime, + lease, + RuntimeSetPositionMoverSetup.ResolvedAbsent)); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out RuntimeInitialCreateResidenceReceipt receipt)); + Assert.Equal(1, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + + IReadOnlyList retirements = + lifetime.BeginSessionClear(); + + Assert.False(lifetime.AcknowledgeInitialCreateResidenceAdoption( + canonical, + receipt.Adoption)); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + RuntimeSetPositionOwnershipSnapshot ownership = + lifetime.Physics.SetPosition.CaptureOwnership(); + Assert.Equal(0, ownership.ActiveOperationCount); + Assert.Equal(0, ownership.PlacementCompletionWatchCount); + Assert.Equal(0, ownership.AcknowledgedPlacementCompletionCount); + foreach (RuntimeEntityRecord record in retirements) + lifetime.CompleteSessionEntityRetirement(record); + Assert.True(lifetime.CompleteSessionClearIfConverged()); + } + + [Fact] + public void PostCompletionRebucketRejectsTopLevelAdoptionAndConverges() + { + var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; + engine.AddLandblock( + Landblock, + new TerrainSurface(new byte[81], new float[256]), + Array.Empty(), + Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + Bind(lifetime, 33UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x7000301Fu, 1, setupId: null), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + AttachDormantBody(lifetime, canonical); + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition + .SubmitPreparedPlacement( + lease.Placement, + Prepare( + lifetime, + lease, + RuntimeSetPositionMoverSetup.ResolvedAbsent)); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out RuntimeInitialCreateResidenceReceipt receipt)); + + lifetime.Entities.SetFullCell( + canonical, + Landblock | 0x0002u, + Landblock); + + Assert.False(lifetime.AcknowledgeInitialCreateResidenceAdoption( + canonical, + receipt.Adoption)); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + RuntimeSetPositionOwnershipSnapshot ownership = + lifetime.Physics.SetPosition.CaptureOwnership(); + Assert.Equal(0, ownership.ActiveOperationCount); + Assert.Equal(0, ownership.PlacementCompletionWatchCount); + Assert.Equal(0, ownership.AcknowledgedPlacementCompletionCount); + } + + [Fact] + public void PostCompletionAuthorityChangeRejectsCelllessAdoption() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 34UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + 0x70003020u, + 1, + includePosition: false, + parentGuid: 0x70004020u), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out RuntimeInitialCreateResidenceReceipt receipt)); + + lifetime.Entities.AdvancePositionAuthority(canonical); + + Assert.False(lifetime.AcknowledgeInitialCreateResidenceAdoption( + canonical, + receipt.Adoption)); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + Assert.True(lifetime.Physics.SetPosition.CaptureOwnership() + .IsConverged); + } + + [Fact] + public void CelllessPendingAdoptionRevisionsForFreshPositionWithoutLeak() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 36UL); + const uint guid = 0x70003024u; + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + guid, + 1, + includePosition: false, + parentGuid: 0x70004024u), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + ulong positionAuthority = canonical.PositionAuthorityVersion; + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out RuntimeInitialCreateResidenceReceipt original)); + int callbacks = 0; + WorldSession.EntityPositionUpdate update = PositionUpdate( + guid, + positionSequence: 2, + teleportSequence: 1, + forcePositionSequence: 0, + positionX: 25f); + + Assert.True(lifetime.TryApplyPosition( + update, + isLocalPlayer: false, + forcePositionRotation: null, + currentLocalVelocity: null, + projectionRequiresTeleportHook: false, + acknowledgeProjection: _ => callbacks++, + out PositionTimestampDisposition disposition, + out _, + out _)); + + Assert.Equal(PositionTimestampDisposition.Apply, disposition); + Assert.Equal(0, callbacks); + Assert.Equal(positionAuthority, canonical.PositionAuthorityVersion); + Assert.Null(canonical.Snapshot.Position); + Assert.Equal(0u, canonical.FullCellId); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out RuntimeInitialCreateResidenceReceipt revised)); + Assert.Equal(original.Adoption.Revision + 1UL, + revised.Adoption.Revision); + RuntimeInitialCreateResidenceContinuation continuation = + Assert.Single(revised.Continuations); + Assert.Equal(25f, continuation.AcceptedWirePosition.PositionX); + Assert.Equal((ushort)1, continuation.AcceptedTeleportSequence); + Assert.False(lifetime.AcknowledgeInitialCreateResidenceAdoption( + canonical, + original.Adoption)); + Assert.True(lifetime.AcknowledgeInitialCreateResidenceAdoption( + canonical, + revised.Adoption)); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + Assert.True(lifetime.Physics.SetPosition.CaptureOwnership() + .IsConverged); + } + + [Fact] + public void ResetSnapshotsAllResidenceOwnersBeforeReentrantDiscardObserver() + { + var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; + engine.AddLandblock( + Landblock, + new TerrainSurface(new byte[81], new float[256]), + Array.Empty(), + Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + Bind(lifetime, 35UL); + RuntimeEntityRecord completedRecord = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + 0x70003023u, + 1, + setupId: null, + positionX: 70f), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + completedRecord, + out RuntimeInitialCreateResidenceLease completedLease)); + AttachDormantBody(lifetime, completedRecord); + RuntimeSetPositionOutcome completedOutcome = lifetime.Physics.SetPosition + .SubmitPreparedPlacement( + completedLease.Placement, + Prepare( + lifetime, + completedLease, + RuntimeSetPositionMoverSetup.ResolvedAbsent)); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + completedOutcome.Projection)); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + completedRecord, + completedLease.Token, + out _)); + + var pending = new List(); + uint[] pendingGuids = [0x70003021u, 0x70003022u]; + for (int index = 0; index < pendingGuids.Length; index++) + { + uint guid = pendingGuids[index]; + RuntimeEntityRecord record = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + guid, + 1, + setupId: null, + positionX: 10f + index * 30f), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + record, + out RuntimeInitialCreateResidenceLease lease)); + AttachDormantBody(lifetime, record); + pending.Add(lifetime.Physics.SetPosition.SubmitPreparedPlacement( + lease.Placement, + Prepare( + lifetime, + lease, + RuntimeSetPositionMoverSetup.ResolvedAbsent))); + } + + var discards = new List(); + int nestedRetirementCount = -1; + using IDisposable subscription = lifetime.Events.SubscribePlacement( + new PlacementObserver(delta => + { + if (delta.Placement.Kind + is not RuntimePlacementProjectionKind.Discard) + { + return; + } + discards.Add(delta.Placement.Token.Sequence); + if (nestedRetirementCount < 0) + { + nestedRetirementCount = + lifetime.BeginSessionClear().Count; + } + })); + + IReadOnlyList retirements = + lifetime.BeginSessionClear(); + + Assert.Equal(0, nestedRetirementCount); + Assert.Equal( + pending.Select(static item => item.Projection.Sequence) + .Order(), + discards.Order()); + Assert.Equal(0, lifetime.Events.DispatchFailureCount); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + foreach (RuntimeEntityRecord record in retirements) + lifetime.CompleteSessionEntityRetirement(record); + Assert.True(lifetime.CompleteSessionClearIfConverged()); + } + + [Fact] + public void InvalidGenerationFailsBeforeInboundOrCanonicalAcceptance() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + ulong generation = 0UL; + lifetime.BindEventContext( + () => new RuntimeGenerationToken(generation), + static () => 1UL); + WorldSession.EntitySpawn spawn = Spawn(0x70003009u, 1); + + Assert.Throws(() => lifetime + .RegisterEntityWithInitialResidence(spawn, isLocalPlayer: false)); + Assert.Equal(0, lifetime.Entities.Count); + Assert.Equal(0, lifetime.Entities.ClaimedLocalIdCount); + Assert.Empty(lifetime.Entities.Snapshots); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + Assert.True(lifetime.Physics.SetPosition.CaptureOwnership() + .IsConverged); + + generation = 14UL; + RuntimeEntityRegistrationResult recovered = lifetime + .RegisterEntityWithInitialResidence(spawn, isLocalPlayer: false); + Assert.Equal(CreateObjectTimestampDisposition.InitialGeneration, + recovered.Inbound.Disposition); + Assert.NotNull(recovered.Canonical); + } + + [Fact] + public void UnboundGenerationFailsWithoutFabricatingSessionAuthority() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + + Assert.Throws(() => lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003014u, 1), + isLocalPlayer: false)); + + Assert.Empty(lifetime.Entities.Snapshots); + Assert.Equal(0, lifetime.Entities.Count); + Assert.Equal(0, lifetime.Entities.ClaimedLocalIdCount); + } + + [Theory] + [InlineData((ushort)1)] + [InlineData((ushort)2)] + public void EqualOrStaleMalformedCreateDoesNotDisplaceCurrentAdmission( + ushort currentIncarnation) + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 24UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003015u, currentIncarnation), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease initial)); + ushort incomingIncarnation = 1; + + RuntimeEntityRegistrationResult result = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + 0x70003015u, + incomingIncarnation, + positionX: float.NaN), + isLocalPlayer: false); + + Assert.Equal( + currentIncarnation == incomingIncarnation + ? CreateObjectTimestampDisposition.ExistingGeneration + : CreateObjectTimestampDisposition.StaleGeneration, + result.Inbound.Disposition); + Assert.True(lifetime.Entities.IsCurrent(canonical)); + Assert.Equal(10f, canonical.Snapshot.Position!.Value.PositionX); + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease retained)); + Assert.Equal(initial, retained); + } + + [Fact] + public void MalformedInitialPositionDoesNotPoisonSameInstanceRecovery() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 15UL); + WorldSession.EntitySpawn malformed = Spawn( + 0x7000300Au, + 1, + positionX: float.NaN); + + Assert.Throws(() => lifetime + .RegisterEntityWithInitialResidence( + malformed, + isLocalPlayer: false)); + Assert.Empty(lifetime.Entities.Snapshots); + Assert.Equal(0, lifetime.Entities.Count); + + WorldSession.EntitySpawn corrected = Spawn(0x7000300Au, 1); + RuntimeEntityRegistrationResult recovered = lifetime + .RegisterEntityWithInitialResidence( + corrected, + isLocalPlayer: false); + Assert.Equal(CreateObjectTimestampDisposition.InitialGeneration, + recovered.Inbound.Disposition); + Assert.Equal(corrected.Position, + recovered.Canonical!.Snapshot.Position); + Assert.Equal(0u, recovered.Canonical.FullCellId); + } + + [Fact] + public void ExistingPlacementCollisionCannotStealOrPartiallyOwnLease() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 16UL); + RuntimeEntityRegistrationResult registration = lifetime.RegisterEntity( + Spawn(0x7000300Bu, 1)); + RuntimeEntityRecord canonical = registration.Canonical!; + lifetime.Entities.SetFullCell(canonical, 0u, 0u); + lifetime.Entities.AdvancePositionAuthority(canonical); + RuntimeEntityPlacementToken conflict = lifetime.Physics.SetPosition + .BeginAuthoredPlacement( + canonical, + canonical.PositionAuthorityVersion, + RuntimeSetPositionOperationKind.RemoteAuthoritative); + Assert.True(conflict.IsValid); + + RuntimeInitialCreateResidenceLease rejected = + lifetime.InitialCreateResidences.Begin( + canonical, + registration.Inbound, + isLocalPlayer: false); + + Assert.False(rejected.IsValid); + Assert.True(lifetime.Physics.SetPosition.IsPlacementCurrent(conflict)); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership() + .PlacementCompletionWatchCount); + } + + [Fact] + public void ParentPrecedesCombinedWirePositionAndCreatesNoWorldPlacement() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 17UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + 0x7000300Cu, + 1, + includePosition: true, + parentGuid: 0x70004001u), + isLocalPlayer: false) + .Canonical!; + + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + Assert.Equal(RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, + lease.Route.Disposition); + Assert.False(lease.Placement.IsValid); + Assert.Equal(0u, canonical.FullCellId); + Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership() + .ActiveOperationCount); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AbsentAndPresentZeroCellCreateAwaitFreshPosition( + bool presentZeroCell) + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 25UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + presentZeroCell ? 0x70003016u : 0x70003017u, + 1, + includePosition: presentZeroCell, + positionCell: 0u), + isLocalPlayer: false) + .Canonical!; + + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + Assert.Equal(RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, + lease.Route.Disposition); + Assert.False(lease.Placement.IsValid); + Assert.Equal(0u, canonical.FullCellId); + Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership() + .ActiveOperationCount); + } + + [Fact] + public void EqualAndNewerSameGenerationCreateDoNotChurnPendingPlacement() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 18UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x7000300Du, 1), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease initial)); + ulong positionAuthority = canonical.PositionAuthorityVersion; + ulong spatialAuthority = canonical.SpatialAuthorityVersion; + + RuntimeEntityRegistrationResult duplicate = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x7000300Du, 1), + isLocalPlayer: false); + Assert.Equal(CreateObjectTimestampDisposition.ExistingGeneration, + duplicate.Inbound.Disposition); + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease afterDuplicate)); + Assert.Equal(initial, afterDuplicate); + Assert.Equal(positionAuthority, canonical.PositionAuthorityVersion); + Assert.Equal(spatialAuthority, canonical.SpatialAuthorityVersion); + + RuntimeEntityRegistrationResult newer = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + 0x7000300Du, + 1, + positionSequence: 2, + positionX: 30f), + isLocalPlayer: false); + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease beforeFreshRoute)); + Assert.Equal(initial, beforeFreshRoute); + 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.Equal(0u, canonical.FullCellId); + Assert.Equal(10f, canonical.Snapshot.Position!.Value.PositionX); + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease successor)); + Assert.Equal(initial.Token, successor.Token); + 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); + Assert.Equal(PositionTimestampDisposition.Apply, + continuation.TimestampDisposition); + Assert.True(successor.Placement.IsValid); + Assert.Equal(1, lifetime.Physics.SetPosition.CaptureOwnership() + .ActiveOperationCount); + } + + [Fact] + public void AlreadyPlacedSameGenerationCreateCannotReclaimWireCell() + { + var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; + engine.AddLandblock( + Landblock, + new TerrainSurface(new byte[81], new float[256]), + Array.Empty(), + Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + Bind(lifetime, 19UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x7000300Eu, 1, setupId: null), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + AttachDormantBody(lifetime, canonical); + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition + .SubmitPreparedPlacement( + lease.Placement, + Prepare( + lifetime, + lease, + RuntimeSetPositionMoverSetup.ResolvedAbsent)); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out _)); + ulong spatialAuthority = canonical.SpatialAuthorityVersion; + + _ = lifetime.RegisterEntityWithInitialResidence( + Spawn(0x7000300Eu, 1, setupId: null), + isLocalPlayer: false); + + Assert.Equal(Cell, canonical.FullCellId); + Assert.Equal(spatialAuthority, canonical.SpatialAuthorityVersion); + Assert.False(lifetime.TryGetInitialCreateResidence(canonical, out _)); + } + + [Fact] + public void LocalNoTeleportFreshPositionRemainsInitialWorldAdmission() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 22UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003012u, 1), + isLocalPlayer: true) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease initial)); + RuntimeEntityRegistrationResult newer = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + 0x70003012u, + 1, + 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.Equal(0u, canonical.FullCellId); + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease successor)); + Assert.Equal(initial.Token, successor.Token); + Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition, + successor.Route.Disposition); + Assert.Equal(RuntimeSetPositionOperationKind.InitialLogin, + successor.Route.OperationKind); + Assert.Equal(RuntimeTeleportHookPhase.AfterEnterWorld, + successor.Route.TeleportHookPhase); + Assert.True(successor.Placement.IsValid); + RuntimeInitialCreateResidenceContinuation continuation = + Assert.Single(successor.Continuations); + Assert.Equal(PositionTimestampDisposition.Apply, + continuation.TimestampDisposition); + Assert.Equal((ushort)0, continuation.PreviousTeleportSequence); + Assert.Equal((ushort)0, continuation.TeleportSequence); + } + + [Fact] + public void LocalForcePositionSuffixIsQueuedBehindInitialAdmission() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 26UL); + RuntimeInitialCreateResidenceLease successor = ApplyFreshSuccessor( + lifetime, + guid: 0x70003018u, + isLocalPlayer: true, + teleportSequence: 0, + forcePositionSequence: 1, + out PositionTimestampDisposition disposition); + + Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition); + Assert.Equal(RuntimeSetPositionOperationKind.InitialLogin, + 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); + } + + [Fact] + public void LocalTeleportSuffixIsQueuedBehindInitialAdmission() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 27UL); + RuntimeInitialCreateResidenceLease successor = ApplyFreshSuccessor( + lifetime, + guid: 0x70003019u, + isLocalPlayer: true, + teleportSequence: 1, + forcePositionSequence: 0, + out PositionTimestampDisposition disposition); + + 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); + } + + [Fact] + public void RemoteTeleportSuffixIsQueuedBehindInitialAdmission() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 28UL); + RuntimeInitialCreateResidenceLease successor = ApplyFreshSuccessor( + lifetime, + guid: 0x7000301Au, + isLocalPlayer: false, + teleportSequence: 1, + forcePositionSequence: 0, + out PositionTimestampDisposition disposition); + + 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); + } + + [Fact] + public void AcceptedPositionsRemainMonotonicRawFifoWithoutMutatingInitialAdmission() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 29UL); + const uint guid = 0x7000301Bu; + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(guid, 1), + isLocalPlayer: true) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease initial)); + ulong positionAuthority = canonical.PositionAuthorityVersion; + int callbacks = 0; + + ApplyQueuedPosition( + lifetime, + guid, + positionSequence: 2, + teleportSequence: 0, + forcePositionSequence: 0, + positionX: 20f, + projectionRequiresTeleportHook: false, + () => callbacks++); + ApplyQueuedPosition( + lifetime, + guid, + positionSequence: 3, + teleportSequence: 1, + forcePositionSequence: 0, + positionX: 30f, + projectionRequiresTeleportHook: true, + () => callbacks++); + ApplyQueuedPosition( + lifetime, + guid, + positionSequence: 4, + teleportSequence: 2, + forcePositionSequence: 0, + positionX: 40f, + projectionRequiresTeleportHook: false, + () => callbacks++); + + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease retained)); + Assert.Equal(initial.Token, retained.Token); + Assert.Equal(initial.Route, retained.Route); + Assert.Equal(initial.Placement, retained.Placement); + Assert.Equal(10f, canonical.Snapshot.Position!.Value.PositionX); + Assert.Equal(positionAuthority, canonical.PositionAuthorityVersion); + Assert.Equal(0, callbacks); + Assert.Equal([1UL, 2UL, 3UL], + retained.Continuations.Select(static item => item.Sequence)); + Assert.Equal([20f, 30f, 40f], + retained.Continuations.Select( + static item => item.AcceptedWirePosition.PositionX)); + Assert.Equal([(ushort)0, (ushort)0, (ushort)1], + retained.Continuations.Select( + static item => 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); + } + + [Fact] + public void MalformedQueuedPositionIsRejectedBeforeTimestampConsumption() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + Bind(lifetime, 30UL); + const uint guid = 0x7000301Cu; + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(guid, 1), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease initial)); + WorldSession.EntityPositionUpdate malformed = PositionUpdate( + guid, + positionSequence: 2, + teleportSequence: 0, + forcePositionSequence: 0, + positionX: float.NaN); + + Assert.False(lifetime.TryApplyPosition( + malformed, + isLocalPlayer: false, + forcePositionRotation: null, + currentLocalVelocity: null, + projectionRequiresTeleportHook: false, + acknowledgeProjection: null, + out PositionTimestampDisposition disposition, + out _, + out _)); + + Assert.Equal(PositionTimestampDisposition.Rejected, disposition); + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease retained)); + Assert.Equal(initial, retained); + Assert.Equal(10f, canonical.Snapshot.Position!.Value.PositionX); + + ApplyQueuedPosition( + lifetime, + guid, + positionSequence: 2, + teleportSequence: 0, + forcePositionSequence: 0, + positionX: 25f, + projectionRequiresTeleportHook: false, + callback: null, + isLocalPlayer: false); + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out retained)); + Assert.Equal((ushort)2, + Assert.Single(retained.Continuations).PositionSequence); + } + + [Fact] + public void CompletionRequiresExactAcknowledgedOriginalPlacement() + { + var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; + engine.AddLandblock( + Landblock, + new TerrainSurface(new byte[81], new float[256]), + Array.Empty(), + Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + Bind(lifetime, 20UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x7000300Fu, 1, setupId: null), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease original)); + AttachDormantBody(lifetime, canonical); + + RuntimeEntityPlacementToken replacement = lifetime.Physics.SetPosition + .BeginAuthoredPlacement( + canonical, + canonical.PositionAuthorityVersion, + RuntimeSetPositionOperationKind.RemoteAuthoritative); + var preparation = new RuntimeSetPositionMoverPreparation( + RuntimeSetPositionMoverSetup.ResolvedAbsent, + RuntimeSetPositionOperationKind.RemoteAuthoritative, + GameTime: 1d, + PhysicsPlacementClass.Ordinary, + original.Route.SetPositionFlags); + Assert.Equal( + RuntimeSetPositionMoverPreparationStatus.Prepared, + lifetime.Physics.SetPosition.PrepareMover( + replacement, + preparation, + out RuntimeSetPositionCommand command)); + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition + .SubmitPreparedPlacement(replacement, command); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); + + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.RejectedAuthority, + lifetime.CompleteInitialCreateResidence( + canonical, + original.Token, + out _)); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership() + .AcknowledgedPlacementCompletionCount); + } + + [Fact] + public void FreshPositionQueuesBehindCommittedUnacknowledgedInitialAdmission() + { + var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; + engine.AddLandblock( + Landblock, + new TerrainSurface(new byte[81], new float[256]), + Array.Empty(), + Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + Bind(lifetime, 23UL); + int deleted = 0; + using IDisposable subscription = lifetime.Events.Subscribe( + new EntityObserver(delta => + { + if (delta.Change is RuntimeEntityChange.Deleted) + deleted++; + })); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003013u, 1, setupId: null), + isLocalPlayer: true) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease initial)); + AttachDormantBody(lifetime, canonical); + RuntimeSetPositionOutcome first = lifetime.Physics.SetPosition + .SubmitPreparedPlacement( + initial.Placement, + Prepare( + lifetime, + initial, + RuntimeSetPositionMoverSetup.ResolvedAbsent)); + Assert.Equal( + RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, + first.Status); + Assert.Equal(Cell, canonical.FullCellId); + + RuntimeEntityRegistrationResult newer = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + 0x70003013u, + 1, + setupId: null, + 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.Equal(0, deleted); + Assert.True(lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot place)); + Assert.Equal(RuntimePlacementProjectionKind.Place, place.Kind); + Assert.Equal(first.Projection.Sequence, place.Token.Sequence); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + place.Token)); + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease successor)); + Assert.Equal(initial.Token, successor.Token); + Assert.Equal(initial.Route, successor.Route); + Assert.Equal(initial.Placement, successor.Placement); + Assert.Equal(RuntimeTeleportHookPhase.AfterEnterWorld, + successor.Route.TeleportHookPhase); + RuntimeInitialCreateResidenceContinuation continuation = + Assert.Single(successor.Continuations); + Assert.Equal(45f, continuation.AcceptedWirePosition.PositionX); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + canonical, + successor.Token, + out RuntimeInitialCreateResidenceReceipt receipt)); + Assert.Equal(RuntimeTeleportHookPhase.AfterEnterWorld, + receipt.TeleportHookPhase); + Assert.Equal(first.Projection, receipt.Projection); + Assert.Equal(successor.Continuations, receipt.Continuations); + ApplyQueuedPosition( + lifetime, + canonical.ServerGuid, + positionSequence: 3, + teleportSequence: 1, + forcePositionSequence: 0, + positionX: 55f, + projectionRequiresTeleportHook: true, + callback: null); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.Completed, + lifetime.CompleteInitialCreateResidence( + canonical, + successor.Token, + out RuntimeInitialCreateResidenceReceipt repeated)); + Assert.Equal(receipt.Adoption.Revision + 1UL, + repeated.Adoption.Revision); + Assert.Equal(2, repeated.Continuations.Length); + Assert.Equal(55f, + repeated.Continuations[1].AcceptedWirePosition.PositionX); + Assert.False(lifetime.AcknowledgeInitialCreateResidenceAdoption( + canonical, + receipt.Adoption)); + Assert.True(lifetime.AcknowledgeInitialCreateResidenceAdoption( + canonical, + repeated.Adoption)); + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.RejectedToken, + lifetime.CompleteInitialCreateResidence( + canonical, + successor.Token, + out _)); + Assert.Equal(0, deleted); + } + + [Fact] + public void PreAcknowledgementSpatialCorruptionRetiresOperationAsDiscard() + { + var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; + engine.AddLandblock( + Landblock, + new TerrainSurface(new byte[81], new float[256]), + Array.Empty(), + Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + Bind(lifetime, 31UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x7000301Du, 1, setupId: null), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + AttachDormantBody(lifetime, canonical); + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition + .SubmitPreparedPlacement( + lease.Placement, + Prepare( + lifetime, + lease, + RuntimeSetPositionMoverSetup.ResolvedAbsent)); + Assert.Equal( + RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, + outcome.Status); + + lifetime.Entities.SetFullCell( + canonical, + Landblock | 0x0002u, + Landblock); + + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.RejectedAuthority, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out _)); + Assert.True(lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot discard)); + Assert.Equal(RuntimePlacementProjectionKind.Discard, discard.Kind); + Assert.Equal(outcome.Projection.Sequence, discard.Token.Sequence); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + discard.Token)); + RuntimeSetPositionOwnershipSnapshot ownership = + lifetime.Physics.SetPosition.CaptureOwnership(); + Assert.Equal(0, ownership.ActiveOperationCount); + Assert.Equal(0, ownership.PlacementCompletionWatchCount); + Assert.Equal(0, ownership.AcknowledgedPlacementCompletionCount); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + } + + [Fact] + public void CorruptedPostAcknowledgementAuthorityRetiresProofAndLease() + { + var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; + engine.AddLandblock( + Landblock, + new TerrainSurface(new byte[81], new float[256]), + Array.Empty(), + Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + Bind(lifetime, 21UL); + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(0x70003010u, 1, setupId: null), + isLocalPlayer: false) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease lease)); + AttachDormantBody(lifetime, canonical); + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition + .SubmitPreparedPlacement( + lease.Placement, + Prepare( + lifetime, + lease, + RuntimeSetPositionMoverSetup.ResolvedAbsent)); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); + lifetime.Entities.AdvancePlacementCommit(canonical); + + Assert.Equal( + RuntimeInitialCreateResidenceCompletionStatus.RejectedAuthority, + lifetime.CompleteInitialCreateResidence( + canonical, + lease.Token, + out _)); + Assert.Equal(0, lifetime.CaptureOwnership() + .InitialCreateResidenceLeaseCount); + RuntimeSetPositionOwnershipSnapshot ownership = + lifetime.Physics.SetPosition.CaptureOwnership(); + Assert.Equal(0, ownership.PlacementCompletionWatchCount); + Assert.Equal(0, ownership.AcknowledgedPlacementCompletionCount); + } + + [Fact] + public void LegacyRegistrationAdvancesSpatialAuthorityOnlyOnce() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + RuntimeEntityRecord canonical = lifetime.RegisterEntity( + Spawn(0x70003011u, 1)).Canonical!; + + Assert.Equal(Cell, canonical.FullCellId); + Assert.Equal(1UL, canonical.SpatialAuthorityVersion); + } + + private static RuntimeSetPositionCommand Prepare( + RuntimeEntityObjectLifetime lifetime, + in RuntimeInitialCreateResidenceLease lease, + in RuntimeSetPositionMoverSetup setup) + { + var preparation = new RuntimeSetPositionMoverPreparation( + setup, + lease.Route.OperationKind, + GameTime: 1d, + PhysicsPlacementClass.Ordinary, + lease.Route.SetPositionFlags); + Assert.Equal( + RuntimeSetPositionMoverPreparationStatus.Prepared, + lifetime.Physics.SetPosition.PrepareMover( + lease.Placement, + preparation, + out RuntimeSetPositionCommand command)); + return command; + } + + private static RuntimeInitialCreateResidenceLease ApplyFreshSuccessor( + RuntimeEntityObjectLifetime lifetime, + uint guid, + bool isLocalPlayer, + ushort teleportSequence, + ushort forcePositionSequence, + out PositionTimestampDisposition disposition) + { + RuntimeEntityRecord canonical = lifetime + .RegisterEntityWithInitialResidence( + Spawn(guid, 1), + isLocalPlayer) + .Canonical!; + Assert.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease initial)); + RuntimeEntityRegistrationResult newer = lifetime + .RegisterEntityWithInitialResidence( + Spawn( + guid, + 1, + positionSequence: 2, + positionX: 40f, + 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.True(lifetime.TryGetInitialCreateResidence( + canonical, + out RuntimeInitialCreateResidenceLease retained)); + Assert.Equal(initial.Token, retained.Token); + Assert.Equal(initial.Route, retained.Route); + Assert.Equal(initial.Placement, retained.Placement); + return retained; + } + + private static void ApplyQueuedPosition( + RuntimeEntityObjectLifetime lifetime, + uint guid, + ushort positionSequence, + ushort teleportSequence, + ushort forcePositionSequence, + float positionX, + bool projectionRequiresTeleportHook, + Action? callback, + bool isLocalPlayer = true) + { + WorldSession.EntityPositionUpdate update = PositionUpdate( + guid, + positionSequence, + teleportSequence, + forcePositionSequence, + positionX); + Assert.True(lifetime.TryApplyPosition( + update, + isLocalPlayer, + forcePositionRotation: Quaternion.Identity, + currentLocalVelocity: new Vector3(1f, 2f, 3f), + projectionRequiresTeleportHook, + acknowledgeProjection: callback is null + ? null + : _ => callback(), + out PositionTimestampDisposition disposition, + out _, + out _)); + Assert.True(disposition is PositionTimestampDisposition.Apply + or PositionTimestampDisposition.ForcePosition); + } + + private static WorldSession.EntityPositionUpdate PositionUpdate( + uint guid, + ushort positionSequence, + ushort teleportSequence, + ushort forcePositionSequence, + float positionX) + { + return new WorldSession.EntityPositionUpdate( + guid, + new CreateObject.ServerPosition( + Cell, + positionX, + 20f, + 7f, + 1f, + 0f, + 0f, + 0f), + new Vector3(positionSequence, 2f, 3f), + PlacementId: positionSequence, + IsGrounded: true, + InstanceSequence: 1, + PositionSequence: positionSequence, + TeleportSequence: teleportSequence, + ForcePositionSequence: forcePositionSequence); + } + + private static void AttachDormantBody( + RuntimeEntityObjectLifetime lifetime, + RuntimeEntityRecord canonical) + { + var body = new PhysicsBody + { + State = canonical.FinalPhysicsState, + Orientation = Quaternion.Identity, + InWorld = false, + }; + lifetime.Entities.SetPhysicsBody(canonical, body); + } + + private static void Bind( + RuntimeEntityObjectLifetime lifetime, + ulong generation) + { + var token = new RuntimeGenerationToken(generation); + lifetime.BindEventContext(() => token, static () => 1UL); + } + + private static WorldSession.EntitySpawn Spawn( + uint guid, + ushort incarnation, + bool missile = false, + bool includePosition = true, + uint? parentGuid = null, + uint? setupId = 0x02000001u, + ushort positionSequence = 1, + float positionX = 10f, + uint positionCell = Cell, + ushort teleportSequence = 0, + ushort forcePositionSequence = 0) + { + CreateObject.ServerPosition? position = includePosition + ? new CreateObject.ServerPosition( + positionCell, + positionX, + 20f, + 7f, + 1f, + 0f, + 0f, + 0f) + : null; + uint rawState = (uint)(PhysicsStateFlags.Gravity + | (missile ? PhysicsStateFlags.Missile : 0)); + var timestamps = new PhysicsTimestamps( + Position: positionSequence, + Movement: 1, + State: 1, + Vector: 1, + Teleport: teleportSequence, + ServerControlledMove: 1, + ForcePosition: forcePositionSequence, + ObjDesc: 1, + Instance: incarnation); + var physics = new PhysicsSpawnData( + rawState, + position, + Movement: null, + AnimationFrame: null, + setupId, + MotionTableId: null, + SoundTableId: null, + PhysicsScriptTableId: null, + Parent: parentGuid is { } parent + ? new PhysicsAttachment(parent, 1u) + : null, + Children: null, + Scale: null, + Friction: null, + Elasticity: null, + Translucency: null, + Velocity: null, + Acceleration: null, + AngularVelocity: null, + DefaultScriptType: null, + DefaultScriptIntensity: null, + timestamps); + return new WorldSession.EntitySpawn( + guid, + position, + setupId, + Array.Empty(), + Array.Empty(), + Array.Empty(), + BasePaletteId: null, + ObjScale: null, + Name: "initial-create", + ItemType: null, + MotionState: null, + MotionTableId: null, + PhysicsState: rawState, + InstanceSequence: incarnation, + PositionSequence: positionSequence, + ParentGuid: parentGuid, + ParentLocation: parentGuid is null ? null : 1u, + Physics: physics); + } + + private sealed class EntityObserver(Action onEntity) + : IRuntimeEntityObjectObserver + { + public void OnEntity(in RuntimeEntityDelta delta) => onEntity(delta); + public void OnInventory(in RuntimeInventoryDelta delta) + { + } + } + + private sealed class PlacementObserver( + Action onPlacement) + : IRuntimePlacementObserver + { + public void OnPlacement(in RuntimePlacementDelta delta) => + onPlacement(delta); + } +} diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs index d895998e..5db0b5fa 100644 --- a/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs @@ -48,7 +48,7 @@ public sealed class RuntimeAuthoritativePositionRouteClassifierTests [Theory] [InlineData(RuntimeCreateResidenceKind.Parented)] [InlineData(RuntimeCreateResidenceKind.PickedUp)] - internal void NewParentedOrPickedUpCreate_LeavesWorldAndAwaitsPosition( + internal void NewParentedOrPickedUpCreate_RemainsCelllessAndAwaitsPosition( RuntimeCreateResidenceKind residence) { RuntimeAuthoritativePositionRoute route = @@ -62,7 +62,7 @@ public sealed class RuntimeAuthoritativePositionRouteClassifierTests Assert.Equal(RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, route.Disposition); - Assert.True(route.LeaveWorld); + Assert.False(route.LeaveWorld); Assert.False(route.PerformsSetPosition); } @@ -294,7 +294,7 @@ public sealed class RuntimeAuthoritativePositionRouteClassifierTests RuntimeAuthoritativePositionRoute route = ClassifyLocal( Authority(ushort.MaxValue, 0)); - Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition, + Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, route.Disposition); Assert.Equal(0x1012u, (uint)route.SetPositionFlags); Assert.True(route.UnparentBeforeRouting);