feat(runtime): freeze initial placement inbound admission
This commit is contained in:
parent
9d601817b8
commit
30012361e1
9 changed files with 2506 additions and 323 deletions
|
|
@ -20,13 +20,35 @@ public sealed class InboundPhysicsStateController
|
|||
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
|
||||
_snapshots.TryGetValue(guid, out spawn);
|
||||
|
||||
internal bool TryGetAcceptedTimestamps(
|
||||
uint guid,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
if (_gates.TryGetValue(guid, out PhysicsTimestampGate? gate))
|
||||
{
|
||||
timestamps = Current(gate);
|
||||
return true;
|
||||
}
|
||||
timestamps = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public CreateObjectTimestampDisposition PreviewCreateDisposition(
|
||||
WorldSession.EntitySpawn incoming) =>
|
||||
_gates.TryGetValue(incoming.Guid, out PhysicsTimestampGate? gate)
|
||||
? gate.PreviewCreateObject(incoming.InstanceSequence)
|
||||
: CreateObjectTimestampDisposition.InitialGeneration;
|
||||
|
||||
public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming)
|
||||
public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming) =>
|
||||
AcceptCreateCore(incoming, deferSameGenerationWeenieDescription: false);
|
||||
|
||||
internal InboundCreateResult AcceptCreateDeferredSameGeneration(
|
||||
WorldSession.EntitySpawn incoming) =>
|
||||
AcceptCreateCore(incoming, deferSameGenerationWeenieDescription: true);
|
||||
|
||||
private InboundCreateResult AcceptCreateCore(
|
||||
WorldSession.EntitySpawn incoming,
|
||||
bool deferSameGenerationWeenieDescription)
|
||||
{
|
||||
if (!_gates.TryGetValue(incoming.Guid, out PhysicsTimestampGate? gate))
|
||||
{
|
||||
|
|
@ -56,8 +78,11 @@ public sealed class InboundPhysicsStateController
|
|||
return new InboundCreateResult(disposition, incoming, null, Current(gate));
|
||||
}
|
||||
|
||||
WorldSession.EntitySpawn merged = MergeUntimestampedCreate(retained, incoming);
|
||||
_snapshots[incoming.Guid] = merged;
|
||||
WorldSession.EntitySpawn merged = deferSameGenerationWeenieDescription
|
||||
? retained
|
||||
: MergeUntimestampedCreate(retained, incoming);
|
||||
if (!deferSameGenerationWeenieDescription)
|
||||
_snapshots[incoming.Guid] = merged;
|
||||
return new InboundCreateResult(
|
||||
disposition,
|
||||
merged,
|
||||
|
|
@ -422,6 +447,181 @@ public sealed class InboundPhysicsStateController
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consumes only the retail Position timestamp gates for a deferred
|
||||
/// initial-Create continuation. The raw PositionPack is not transformed
|
||||
/// and no pose, parent, placement, or velocity field is installed. The
|
||||
/// returned authority is sufficient for one later execution without
|
||||
/// running the gate a second time.
|
||||
/// </summary>
|
||||
internal bool TryAcceptDeferredPosition(
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
bool isLocalPlayer,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out AcceptedPhysicsTimestamps timestamps,
|
||||
out bool hasTimestampMutation)
|
||||
{
|
||||
if (!TryGet(
|
||||
update.Guid,
|
||||
out PhysicsTimestampGate? gate,
|
||||
out WorldSession.EntitySpawn old))
|
||||
{
|
||||
disposition = PositionTimestampDisposition.Rejected;
|
||||
timestamps = default;
|
||||
hasTimestampMutation = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
ushort previousPosition = gate.PositionTimestamp;
|
||||
ushort previousTeleport = gate.TeleportTimestamp;
|
||||
ushort previousForcePosition = gate.ForcePositionTimestamp;
|
||||
bool advancesTeleport = PhysicsTimestampGate.IsNewer(
|
||||
previousTeleport,
|
||||
update.TeleportSequence);
|
||||
disposition = gate.TryAcceptPositionEvent(
|
||||
update.InstanceSequence,
|
||||
update.PositionSequence,
|
||||
update.TeleportSequence,
|
||||
update.ForcePositionSequence,
|
||||
isLocalPlayer);
|
||||
timestamps = Current(
|
||||
gate,
|
||||
teleportAdvanced: disposition is PositionTimestampDisposition.Apply
|
||||
&& advancesTeleport,
|
||||
previousTeleport: previousTeleport);
|
||||
hasTimestampMutation = previousPosition != gate.PositionTimestamp
|
||||
|| previousTeleport != gate.TeleportTimestamp
|
||||
|| previousForcePosition != gate.ForcePositionTimestamp;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool TryAcceptDeferredObjDesc(
|
||||
ObjDescEvent.Parsed update,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out _)
|
||||
|| !gate.TryAcceptObjDescEvent(
|
||||
update.InstanceSequence,
|
||||
update.ObjDescSequence))
|
||||
{
|
||||
timestamps = default;
|
||||
return false;
|
||||
}
|
||||
timestamps = Current(gate);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool TryAcceptDeferredPickup(
|
||||
PickupEvent.Parsed update,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out _)
|
||||
|| !gate.TryAcceptPositionChannelEvent(
|
||||
update.InstanceSequence,
|
||||
update.PositionSequence))
|
||||
{
|
||||
timestamps = default;
|
||||
return false;
|
||||
}
|
||||
timestamps = Current(gate);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool TryAcceptDeferredCreateParent(
|
||||
CreateParentUpdate update,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
if (!TryGet(update.ChildGuid, out PhysicsTimestampGate? gate, out _)
|
||||
|| !gate.TryAcceptPositionChannelEvent(
|
||||
update.ChildInstanceSequence,
|
||||
update.ChildPositionSequence))
|
||||
{
|
||||
timestamps = default;
|
||||
return false;
|
||||
}
|
||||
timestamps = Current(gate);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool TryAcceptDeferredParent(
|
||||
ParentEvent.Parsed update,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
if (!_gates.TryGetValue(
|
||||
update.ParentGuid,
|
||||
out PhysicsTimestampGate? parentGate)
|
||||
|| !parentGate.IsCurrentInstance(update.ParentInstanceSequence)
|
||||
|| !TryGet(
|
||||
update.ChildGuid,
|
||||
out PhysicsTimestampGate? childGate,
|
||||
out _)
|
||||
|| !childGate.TryAcceptPositionChannelEvent(
|
||||
childGate.InstanceTimestamp,
|
||||
update.ChildPositionSequence))
|
||||
{
|
||||
timestamps = default;
|
||||
return false;
|
||||
}
|
||||
timestamps = Current(childGate);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool TryAcceptDeferredMotion(
|
||||
WorldSession.EntityMotionUpdate update,
|
||||
out AcceptedPhysicsTimestamps timestamps,
|
||||
out bool hasTimestampMutation)
|
||||
{
|
||||
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out _))
|
||||
{
|
||||
timestamps = default;
|
||||
hasTimestampMutation = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
ushort previousMovement = gate.MovementTimestamp;
|
||||
ushort previousServerControl = gate.ServerControlledMoveTimestamp;
|
||||
bool accepted = gate.TryAcceptMovementEvent(
|
||||
update.InstanceSequence,
|
||||
update.MovementSequence,
|
||||
update.ServerControlSequence);
|
||||
timestamps = Current(gate);
|
||||
hasTimestampMutation = previousMovement != gate.MovementTimestamp
|
||||
|| previousServerControl != gate.ServerControlledMoveTimestamp;
|
||||
return accepted;
|
||||
}
|
||||
|
||||
internal bool TryAcceptDeferredState(
|
||||
SetState.Parsed update,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out _)
|
||||
|| !gate.TryAcceptStateEvent(
|
||||
update.InstanceSequence,
|
||||
update.StateSequence))
|
||||
{
|
||||
timestamps = default;
|
||||
return false;
|
||||
}
|
||||
timestamps = Current(gate);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool TryAcceptDeferredVector(
|
||||
VectorUpdate.Parsed update,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out _)
|
||||
|| !gate.TryAcceptVectorEvent(
|
||||
update.InstanceSequence,
|
||||
update.VectorSequence))
|
||||
{
|
||||
timestamps = default;
|
||||
return false;
|
||||
}
|
||||
timestamps = Current(gate);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F751 is a notification gate only. Retail compares it to TELEPORT_TS but
|
||||
/// advances that timestamp later, with the accepted Position packet.
|
||||
|
|
@ -465,12 +665,15 @@ public sealed class InboundPhysicsStateController
|
|||
|
||||
private static AcceptedPhysicsTimestamps Current(
|
||||
PhysicsTimestampGate gate,
|
||||
bool teleportAdvanced = false) => new(
|
||||
bool teleportAdvanced = false,
|
||||
ushort previousTeleport = 0) => new(
|
||||
gate.InstanceTimestamp,
|
||||
gate.ServerControlledMoveTimestamp,
|
||||
gate.TeleportTimestamp,
|
||||
gate.ForcePositionTimestamp,
|
||||
teleportAdvanced);
|
||||
teleportAdvanced,
|
||||
TeleportHookRequired: false,
|
||||
previousTeleport);
|
||||
|
||||
private static WorldSession.EntitySpawn MirrorGateTimestamps(
|
||||
WorldSession.EntitySpawn spawn,
|
||||
|
|
@ -671,7 +874,8 @@ public readonly record struct AcceptedPhysicsTimestamps(
|
|||
ushort Teleport,
|
||||
ushort ForcePosition,
|
||||
bool TeleportAdvanced = false,
|
||||
bool TeleportHookRequired = false);
|
||||
bool TeleportHookRequired = false,
|
||||
ushort PreviousTeleport = 0);
|
||||
|
||||
public readonly record struct CreateParentUpdate(
|
||||
uint ChildGuid,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
|
|
@ -17,12 +18,119 @@ public sealed class ParentAttachmentState
|
|||
private readonly Dictionary<uint, ParentAttachmentRelation> _recoveryByChild = new();
|
||||
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = new();
|
||||
private readonly Dictionary<ParentIncarnation, List<uint>> _committedChildrenByParent = new();
|
||||
private readonly Dictionary<uint, Queue<DeferredParentCreate>>
|
||||
_deferredCreatesByParent = [];
|
||||
private ulong _nextDeferredCreateAdmissionId;
|
||||
|
||||
public int UnresolvedRelationCount =>
|
||||
_unresolvedByChild.Values.Sum(queue => queue.Count);
|
||||
public int StagedRelationCount => _stagedByChild.Count;
|
||||
public int RecoveryRelationCount => _recoveryByChild.Count;
|
||||
public int CommittedRelationCount => _lastAcceptedByChild.Count;
|
||||
internal int DeferredCreateCount =>
|
||||
_deferredCreatesByParent.Values.Sum(queue => queue.Count);
|
||||
|
||||
/// <summary>
|
||||
/// Retains the complete unaccepted CreateObject packet when its nonzero
|
||||
/// parent is not addressable. Retail queues the raw blob before object
|
||||
/// lookup and before any child timestamp is consumed; storing a decoded
|
||||
/// relation after AcceptCreate would partially admit the child.
|
||||
/// </summary>
|
||||
internal void EnqueueDeferredCreate(
|
||||
WorldSession.EntitySpawn spawn,
|
||||
bool isLocalPlayer)
|
||||
{
|
||||
uint parentGuid = spawn.ParentGuid
|
||||
?? spawn.Physics?.Parent?.Guid
|
||||
?? 0u;
|
||||
if (spawn.Guid == 0u || parentGuid == 0u)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"A deferred parent CreateObject requires nonzero child and parent GUIDs.",
|
||||
nameof(spawn));
|
||||
}
|
||||
if (_nextDeferredCreateAdmissionId == ulong.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The deferred parent CreateObject admission sequence is exhausted.");
|
||||
}
|
||||
if (!_deferredCreatesByParent.TryGetValue(
|
||||
parentGuid,
|
||||
out Queue<DeferredParentCreate>? queue))
|
||||
{
|
||||
queue = new Queue<DeferredParentCreate>();
|
||||
_deferredCreatesByParent.Add(parentGuid, queue);
|
||||
}
|
||||
ulong admissionId = _nextDeferredCreateAdmissionId + 1UL;
|
||||
queue.Enqueue(new DeferredParentCreate(
|
||||
admissionId,
|
||||
RuntimeInitialCreateAdmissionFreezer.Freeze(spawn),
|
||||
isLocalPlayer));
|
||||
_nextDeferredCreateAdmissionId = admissionId;
|
||||
}
|
||||
|
||||
internal bool TryPeekDeferredCreate(
|
||||
uint parentGuid,
|
||||
out DeferredParentCreate deferred)
|
||||
{
|
||||
if (!_deferredCreatesByParent.TryGetValue(
|
||||
parentGuid,
|
||||
out Queue<DeferredParentCreate>? queue)
|
||||
|| !queue.TryPeek(out deferred))
|
||||
{
|
||||
deferred = default;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool ConsumeDeferredCreate(
|
||||
uint parentGuid,
|
||||
in DeferredParentCreate expected)
|
||||
{
|
||||
if (!_deferredCreatesByParent.TryGetValue(
|
||||
parentGuid,
|
||||
out Queue<DeferredParentCreate>? queue)
|
||||
|| !queue.TryPeek(out DeferredParentCreate current)
|
||||
|| current != expected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_ = queue.Dequeue();
|
||||
if (queue.Count == 0)
|
||||
_deferredCreatesByParent.Remove(parentGuid);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool ContainsDeferredCreate(
|
||||
uint childGuid,
|
||||
ushort instanceSequence)
|
||||
{
|
||||
foreach (Queue<DeferredParentCreate> queue
|
||||
in _deferredCreatesByParent.Values)
|
||||
{
|
||||
if (queue.Any(candidate =>
|
||||
candidate.Spawn.Guid == childGuid
|
||||
&& candidate.Spawn.InstanceSequence == instanceSequence))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels only the raw, still-unaccepted child generation addressed by a
|
||||
/// terminal packet. Instance zero is a normal retail timestamp and is not
|
||||
/// treated as an empty sentinel.
|
||||
/// </summary>
|
||||
internal void CancelDeferredChildGeneration(
|
||||
uint childGuid,
|
||||
ushort terminalInstanceSequence) => FilterDeferredCreates(
|
||||
candidate => candidate.Spawn.Guid != childGuid
|
||||
|| PhysicsTimestampGate.IsNewer(
|
||||
terminalInstanceSequence,
|
||||
candidate.Spawn.InstanceSequence));
|
||||
|
||||
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
|
||||
{
|
||||
|
|
@ -270,6 +378,7 @@ public sealed class ParentAttachmentState
|
|||
|
||||
public void RemoveObject(uint guid)
|
||||
{
|
||||
RemoveDeferredChildCreates(guid);
|
||||
_stagedByChild.Remove(guid);
|
||||
_recoveryByChild.Remove(guid);
|
||||
RemoveCommittedChild(guid);
|
||||
|
|
@ -298,6 +407,12 @@ public sealed class ParentAttachmentState
|
|||
/// </summary>
|
||||
public void EndGeneration(uint guid, ushort replacementGeneration)
|
||||
{
|
||||
FilterDeferredCreates(candidate =>
|
||||
candidate.Spawn.Guid != guid
|
||||
|| candidate.Spawn.InstanceSequence == replacementGeneration
|
||||
|| PhysicsTimestampGate.IsNewer(
|
||||
replacementGeneration,
|
||||
candidate.Spawn.InstanceSequence));
|
||||
FilterChildCandidates(
|
||||
guid,
|
||||
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
||||
|
|
@ -322,6 +437,7 @@ public sealed class ParentAttachmentState
|
|||
/// </summary>
|
||||
public void DeleteGeneration(uint guid, ushort deletedGeneration)
|
||||
{
|
||||
CancelDeferredChildGeneration(guid, deletedGeneration);
|
||||
FilterChildCandidates(
|
||||
guid,
|
||||
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
||||
|
|
@ -351,6 +467,7 @@ public sealed class ParentAttachmentState
|
|||
|
||||
public void RemoveChild(uint childGuid)
|
||||
{
|
||||
RemoveDeferredChildCreates(childGuid);
|
||||
_stagedByChild.Remove(childGuid);
|
||||
_recoveryByChild.Remove(childGuid);
|
||||
RemoveCommittedChild(childGuid);
|
||||
|
|
@ -359,6 +476,7 @@ public sealed class ParentAttachmentState
|
|||
|
||||
public void Clear()
|
||||
{
|
||||
_deferredCreatesByParent.Clear();
|
||||
_unresolvedByChild.Clear();
|
||||
_stagedByChild.Clear();
|
||||
_recoveryByChild.Clear();
|
||||
|
|
@ -368,6 +486,26 @@ public sealed class ParentAttachmentState
|
|||
_committedChildrenByParent.Clear();
|
||||
}
|
||||
|
||||
private void RemoveDeferredChildCreates(uint childGuid)
|
||||
=> FilterDeferredCreates(
|
||||
candidate => candidate.Spawn.Guid != childGuid);
|
||||
|
||||
private void FilterDeferredCreates(
|
||||
Func<DeferredParentCreate, bool> retain)
|
||||
{
|
||||
uint[] parents = _deferredCreatesByParent.Keys.ToArray();
|
||||
for (int index = 0; index < parents.Length; index++)
|
||||
{
|
||||
uint parentGuid = parents[index];
|
||||
Queue<DeferredParentCreate> retained = new(
|
||||
_deferredCreatesByParent[parentGuid].Where(retain));
|
||||
if (retained.Count == 0)
|
||||
_deferredCreatesByParent.Remove(parentGuid);
|
||||
else
|
||||
_deferredCreatesByParent[parentGuid] = retained;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveCommittedChild(uint childGuid)
|
||||
{
|
||||
if (!_lastAcceptedByChild.Remove(
|
||||
|
|
@ -462,6 +600,15 @@ public sealed class ParentAttachmentState
|
|||
ushort InstanceSequence);
|
||||
}
|
||||
|
||||
internal readonly record struct DeferredParentCreate(
|
||||
ulong AdmissionId,
|
||||
WorldSession.EntitySpawn Spawn,
|
||||
bool IsLocalPlayer)
|
||||
{
|
||||
internal bool IsValid => AdmissionId != 0UL
|
||||
&& Spawn.Guid != 0u;
|
||||
}
|
||||
|
||||
public readonly record struct ParentAttachmentRelation(
|
||||
uint ParentGuid,
|
||||
uint ChildGuid,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Physics;
|
||||
|
||||
|
|
@ -47,6 +48,10 @@ public sealed class RuntimeEntityDirectory
|
|||
public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming) =>
|
||||
_inbound.AcceptCreate(incoming);
|
||||
|
||||
internal InboundCreateResult AcceptCreateDeferredSameGeneration(
|
||||
WorldSession.EntitySpawn incoming) =>
|
||||
_inbound.AcceptCreateDeferredSameGeneration(incoming);
|
||||
|
||||
public CreateObjectTimestampDisposition PreviewCreateDisposition(
|
||||
WorldSession.EntitySpawn incoming) =>
|
||||
_inbound.PreviewCreateDisposition(incoming);
|
||||
|
|
@ -59,6 +64,11 @@ public sealed class RuntimeEntityDirectory
|
|||
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
|
||||
_inbound.TryGetSnapshot(guid, out spawn);
|
||||
|
||||
internal bool TryGetAcceptedTimestamps(
|
||||
uint guid,
|
||||
out AcceptedPhysicsTimestamps timestamps) =>
|
||||
_inbound.TryGetAcceptedTimestamps(guid, out timestamps);
|
||||
|
||||
public bool TryGetActive(uint guid, out RuntimeEntityRecord record) =>
|
||||
_activeByGuid.TryGetValue(guid, out record!);
|
||||
|
||||
|
|
@ -482,6 +492,58 @@ public sealed class RuntimeEntityDirectory
|
|||
out accepted,
|
||||
out timestamps);
|
||||
|
||||
internal bool TryAcceptDeferredPosition(
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
bool isLocalPlayer,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out AcceptedPhysicsTimestamps timestamps,
|
||||
out bool hasTimestampMutation) =>
|
||||
_inbound.TryAcceptDeferredPosition(
|
||||
update,
|
||||
isLocalPlayer,
|
||||
out disposition,
|
||||
out timestamps,
|
||||
out hasTimestampMutation);
|
||||
|
||||
internal bool TryAcceptDeferredObjDesc(
|
||||
ObjDescEvent.Parsed update,
|
||||
out AcceptedPhysicsTimestamps timestamps) =>
|
||||
_inbound.TryAcceptDeferredObjDesc(update, out timestamps);
|
||||
|
||||
internal bool TryAcceptDeferredPickup(
|
||||
PickupEvent.Parsed update,
|
||||
out AcceptedPhysicsTimestamps timestamps) =>
|
||||
_inbound.TryAcceptDeferredPickup(update, out timestamps);
|
||||
|
||||
internal bool TryAcceptDeferredCreateParent(
|
||||
CreateParentUpdate update,
|
||||
out AcceptedPhysicsTimestamps timestamps) =>
|
||||
_inbound.TryAcceptDeferredCreateParent(update, out timestamps);
|
||||
|
||||
internal bool TryAcceptDeferredParent(
|
||||
ParentEvent.Parsed update,
|
||||
out AcceptedPhysicsTimestamps timestamps) =>
|
||||
_inbound.TryAcceptDeferredParent(update, out timestamps);
|
||||
|
||||
internal bool TryAcceptDeferredMotion(
|
||||
WorldSession.EntityMotionUpdate update,
|
||||
out AcceptedPhysicsTimestamps timestamps,
|
||||
out bool hasTimestampMutation) =>
|
||||
_inbound.TryAcceptDeferredMotion(
|
||||
update,
|
||||
out timestamps,
|
||||
out hasTimestampMutation);
|
||||
|
||||
internal bool TryAcceptDeferredState(
|
||||
SetState.Parsed update,
|
||||
out AcceptedPhysicsTimestamps timestamps) =>
|
||||
_inbound.TryAcceptDeferredState(update, out timestamps);
|
||||
|
||||
internal bool TryAcceptDeferredVector(
|
||||
VectorUpdate.Parsed update,
|
||||
out AcceptedPhysicsTimestamps timestamps) =>
|
||||
_inbound.TryAcceptDeferredVector(update, out timestamps);
|
||||
|
||||
public bool IsFreshTeleportStart(uint guid, ushort teleportSequence) =>
|
||||
_inbound.IsFreshTeleportStart(guid, teleportSequence);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Immutable;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
|
@ -11,7 +12,8 @@ public readonly record struct RuntimeEntityRegistrationResult(
|
|||
RuntimeEntityRecord? Canonical,
|
||||
bool LogicalRegistrationCreated,
|
||||
bool ReplacedExistingGeneration,
|
||||
Exception? PriorGenerationCleanupFailure = null);
|
||||
Exception? PriorGenerationCleanupFailure = null,
|
||||
bool DeferredForParent = false);
|
||||
|
||||
public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
||||
int ActiveEntityCount,
|
||||
|
|
@ -19,6 +21,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
|||
int ClaimedLocalIdCount,
|
||||
int AcceptedSnapshotCount,
|
||||
int UnresolvedParentRelationCount,
|
||||
int DeferredParentCreateCount,
|
||||
int StagedParentRelationCount,
|
||||
int RecoveryParentRelationCount,
|
||||
int CommittedParentRelationCount,
|
||||
|
|
@ -44,6 +47,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
|||
&& ClaimedLocalIdCount == 0
|
||||
&& AcceptedSnapshotCount == 0
|
||||
&& UnresolvedParentRelationCount == 0
|
||||
&& DeferredParentCreateCount == 0
|
||||
&& StagedParentRelationCount == 0
|
||||
&& RecoveryParentRelationCount == 0
|
||||
&& CommittedParentRelationCount == 0
|
||||
|
|
@ -205,6 +209,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
Entities.ClaimedLocalIdCount,
|
||||
Entities.Snapshots.Count,
|
||||
parents.UnresolvedRelationCount,
|
||||
parents.DeferredCreateCount,
|
||||
parents.StagedRelationCount,
|
||||
parents.RecoveryRelationCount,
|
||||
parents.CommittedRelationCount,
|
||||
|
|
@ -272,6 +277,35 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
throw new InvalidOperationException(
|
||||
"A Runtime entity cannot register while its session lifetime is clearing.");
|
||||
}
|
||||
if (beginInitialResidence
|
||||
&& !HasConsistentCreateIdentityAndParent(incoming))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"CreateObject 0x{incoming.Guid:X8} has inconsistent instance or parent projections.");
|
||||
}
|
||||
if (beginInitialResidence)
|
||||
incoming = RuntimeInitialCreateAdmissionFreezer.Freeze(incoming);
|
||||
uint parentGuid = incoming.ParentGuid
|
||||
?? incoming.Physics?.Parent?.Guid
|
||||
?? 0u;
|
||||
if (beginInitialResidence
|
||||
&& parentGuid != 0u
|
||||
&& !Entities.TryGetActive(parentGuid, out _))
|
||||
{
|
||||
// SmartBox::HandleCreateObject resolves a nonzero parent before
|
||||
// object lookup or timestamp admission. Retain the complete raw
|
||||
// CreateObject so no child gate/canonical/projection state can
|
||||
// escape before the parent becomes addressable.
|
||||
Entities.ParentAttachments.EnqueueDeferredCreate(
|
||||
incoming,
|
||||
isLocalPlayer);
|
||||
return new RuntimeEntityRegistrationResult(
|
||||
SupersededCreateResult(),
|
||||
Canonical: null,
|
||||
LogicalRegistrationCreated: false,
|
||||
ReplacedExistingGeneration: false,
|
||||
DeferredForParent: true);
|
||||
}
|
||||
CreateObjectTimestampDisposition preview =
|
||||
Entities.PreviewCreateDisposition(incoming);
|
||||
bool requiresFreshResidenceAdmission = preview is
|
||||
|
|
@ -285,7 +319,44 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
$"CreateObject 0x{incoming.Guid:X8} cannot acquire a structurally valid initial residence lease.");
|
||||
}
|
||||
|
||||
InboundCreateResult result = Entities.AcceptCreate(incoming);
|
||||
RuntimeEntityRecord? pendingResidenceRecord = null;
|
||||
RuntimeInitialCreateResidenceLease pendingResidence = default;
|
||||
bool admitIntoPendingResidence = beginInitialResidence
|
||||
&& preview is CreateObjectTimestampDisposition.ExistingGeneration
|
||||
&& Entities.TryGetActive(
|
||||
incoming.Guid,
|
||||
out pendingResidenceRecord)
|
||||
&& InitialCreateResidences.TryGetTransaction(
|
||||
pendingResidenceRecord,
|
||||
out pendingResidence);
|
||||
if (admitIntoPendingResidence
|
||||
&& !InitialCreateResidences.CanEnqueue(
|
||||
pendingResidenceRecord!,
|
||||
pendingResidence))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"CreateObject 0x{incoming.Guid:X8} cannot append to its pending initial residence FIFO.");
|
||||
}
|
||||
if (admitIntoPendingResidence
|
||||
&& !IsStructurallyValidDeferredCreate(incoming))
|
||||
{
|
||||
_ = Entities.TryGetAcceptedTimestamps(
|
||||
incoming.Guid,
|
||||
out AcceptedPhysicsTimestamps timestamps);
|
||||
return new RuntimeEntityRegistrationResult(
|
||||
new InboundCreateResult(
|
||||
CreateObjectTimestampDisposition.ExistingGeneration,
|
||||
pendingResidenceRecord!.Snapshot,
|
||||
SameGenerationEvents: null,
|
||||
timestamps),
|
||||
pendingResidenceRecord,
|
||||
LogicalRegistrationCreated: false,
|
||||
ReplacedExistingGeneration: false);
|
||||
}
|
||||
|
||||
InboundCreateResult result = admitIntoPendingResidence
|
||||
? Entities.AcceptCreateDeferredSameGeneration(incoming)
|
||||
: Entities.AcceptCreate(incoming);
|
||||
if (result.Disposition
|
||||
is CreateObjectTimestampDisposition.StaleGeneration)
|
||||
{
|
||||
|
|
@ -307,6 +378,33 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
incoming.Guid,
|
||||
out RuntimeEntityRecord retained))
|
||||
{
|
||||
if (admitIntoPendingResidence)
|
||||
{
|
||||
if (!ReferenceEquals(retained, pendingResidenceRecord)
|
||||
|| !AdmitSameGenerationCreate(
|
||||
retained,
|
||||
pendingResidence,
|
||||
incoming,
|
||||
result,
|
||||
isLocalPlayer))
|
||||
{
|
||||
throw FailInitialResidenceRegistration(
|
||||
retained,
|
||||
publishDeleted: true);
|
||||
}
|
||||
|
||||
InboundCreateResult dormant = result with
|
||||
{
|
||||
Snapshot = retained.Snapshot,
|
||||
SameGenerationEvents = null,
|
||||
};
|
||||
return new RuntimeEntityRegistrationResult(
|
||||
dormant,
|
||||
retained,
|
||||
LogicalRegistrationCreated: false,
|
||||
ReplacedExistingGeneration: false);
|
||||
}
|
||||
|
||||
// Existing-generation CreateObject contributes untimestamped
|
||||
// description fields here. Position/Parent/Pickup/State/etc.
|
||||
// remain separate freshness-gated events and must not churn a
|
||||
|
|
@ -592,6 +690,33 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (TryGetPendingInitialResidence(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord pending,
|
||||
out RuntimeInitialCreateResidenceLease lease))
|
||||
{
|
||||
if (!InitialCreateResidences.CanEnqueue(pending, lease))
|
||||
{
|
||||
accepted = pending.Snapshot;
|
||||
return false;
|
||||
}
|
||||
bool acceptedByGate = Entities.TryAcceptDeferredObjDesc(
|
||||
update,
|
||||
out _);
|
||||
accepted = pending.Snapshot;
|
||||
if (!acceptedByGate)
|
||||
return false;
|
||||
EnqueueDormant(
|
||||
pending,
|
||||
lease,
|
||||
RuntimeInitialCreateContinuationKind.ObjDesc,
|
||||
RuntimeAcceptedPositionSource.Unknown,
|
||||
new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.ObjDesc,
|
||||
update.Guid,
|
||||
ObjDesc: update));
|
||||
return true;
|
||||
}
|
||||
bool applied = Entities.TryApplyObjDesc(update, out accepted);
|
||||
if (!applied
|
||||
|| !Entities.TryGetActive(
|
||||
|
|
@ -617,6 +742,33 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (TryGetPendingInitialResidence(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord pending,
|
||||
out RuntimeInitialCreateResidenceLease lease))
|
||||
{
|
||||
if (!InitialCreateResidences.CanEnqueue(pending, lease))
|
||||
{
|
||||
accepted = pending.Snapshot;
|
||||
return false;
|
||||
}
|
||||
bool acceptedByGate = Entities.TryAcceptDeferredPickup(
|
||||
update,
|
||||
out _);
|
||||
accepted = pending.Snapshot;
|
||||
if (!acceptedByGate)
|
||||
return false;
|
||||
EnqueueDormant(
|
||||
pending,
|
||||
lease,
|
||||
RuntimeInitialCreateContinuationKind.Pickup,
|
||||
RuntimeAcceptedPositionSource.Unknown,
|
||||
new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.Pickup,
|
||||
update.Guid,
|
||||
Pickup: update));
|
||||
return true;
|
||||
}
|
||||
bool applied = Entities.TryApplyPickup(update, out accepted);
|
||||
if (!applied
|
||||
|| !Entities.TryGetActive(
|
||||
|
|
@ -655,6 +807,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (TryGetPendingInitialResidence(
|
||||
update.ChildGuid,
|
||||
out _,
|
||||
out _))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A CreateObject parent relation must be admitted inside its atomic same-generation Create envelope.");
|
||||
}
|
||||
bool applied = Entities.TryApplyCreateParent(update, out accepted);
|
||||
return CommitPositionChannelUpdate(
|
||||
applied,
|
||||
|
|
@ -669,6 +829,44 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (TryGetPendingInitialResidence(
|
||||
update.ChildGuid,
|
||||
out RuntimeEntityRecord pending,
|
||||
out RuntimeInitialCreateResidenceLease lease))
|
||||
{
|
||||
if (!InitialCreateResidences.CanEnqueue(pending, lease))
|
||||
{
|
||||
accepted = pending.Snapshot;
|
||||
return false;
|
||||
}
|
||||
if (!Entities.TryGetActive(
|
||||
update.ParentGuid,
|
||||
out RuntimeEntityRecord parent)
|
||||
|| parent.Incarnation != update.ParentInstanceSequence)
|
||||
{
|
||||
Entities.ParentAttachments.Enqueue(update);
|
||||
accepted = pending.Snapshot;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool acceptedByGate = Entities.TryAcceptDeferredParent(
|
||||
update,
|
||||
out AcceptedPhysicsTimestamps timestamps);
|
||||
accepted = pending.Snapshot;
|
||||
if (!acceptedByGate)
|
||||
return false;
|
||||
EnqueueDormant(
|
||||
pending,
|
||||
lease,
|
||||
RuntimeInitialCreateContinuationKind.Parent,
|
||||
RuntimeAcceptedPositionSource.Unknown,
|
||||
new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.Parent,
|
||||
update.ChildGuid,
|
||||
Parent: update,
|
||||
AcceptedTimestamps: timestamps));
|
||||
return true;
|
||||
}
|
||||
bool applied = Entities.TryApplyParent(update, out accepted);
|
||||
return CommitPositionChannelUpdate(
|
||||
applied,
|
||||
|
|
@ -750,6 +948,39 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (TryGetPendingInitialResidence(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord pending,
|
||||
out RuntimeInitialCreateResidenceLease lease))
|
||||
{
|
||||
if (!InitialCreateResidences.CanEnqueue(pending, lease))
|
||||
{
|
||||
accepted = pending.Snapshot;
|
||||
timestamps = default;
|
||||
return false;
|
||||
}
|
||||
bool payloadApplied = Entities.TryAcceptDeferredMotion(
|
||||
update,
|
||||
out timestamps,
|
||||
out bool timestampMutation);
|
||||
accepted = pending.Snapshot;
|
||||
if (!payloadApplied && !timestampMutation)
|
||||
return false;
|
||||
EnqueueDormant(
|
||||
pending,
|
||||
lease,
|
||||
RuntimeInitialCreateContinuationKind.Movement,
|
||||
RuntimeAcceptedPositionSource.Unknown,
|
||||
new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.Movement,
|
||||
update.Guid,
|
||||
Movement: update,
|
||||
AcceptedTimestamps: timestamps,
|
||||
AppliesMovementPayload: payloadApplied,
|
||||
RetainMovementPayload: retainPayload,
|
||||
HasTimestampMutation: timestampMutation));
|
||||
return payloadApplied;
|
||||
}
|
||||
bool applied = Entities.TryApplyMotion(
|
||||
update,
|
||||
retainPayload,
|
||||
|
|
@ -790,6 +1021,35 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (TryGetPendingInitialResidence(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord pending,
|
||||
out RuntimeInitialCreateResidenceLease lease))
|
||||
{
|
||||
if (!IsFinite(update.Velocity)
|
||||
|| !IsFinite(update.Omega)
|
||||
|| !InitialCreateResidences.CanEnqueue(pending, lease))
|
||||
{
|
||||
accepted = pending.Snapshot;
|
||||
return false;
|
||||
}
|
||||
bool acceptedByGate = Entities.TryAcceptDeferredVector(
|
||||
update,
|
||||
out _);
|
||||
accepted = pending.Snapshot;
|
||||
if (!acceptedByGate)
|
||||
return false;
|
||||
EnqueueDormant(
|
||||
pending,
|
||||
lease,
|
||||
RuntimeInitialCreateContinuationKind.Vector,
|
||||
RuntimeAcceptedPositionSource.Unknown,
|
||||
new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.Vector,
|
||||
update.Guid,
|
||||
Vector: update));
|
||||
return true;
|
||||
}
|
||||
bool applied = Entities.TryApplyVector(update, out accepted);
|
||||
if (!applied
|
||||
|| !Entities.TryGetActive(
|
||||
|
|
@ -817,6 +1077,35 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
out RetailPhysicsStateTransition transition)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (TryGetPendingInitialResidence(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord pending,
|
||||
out RuntimeInitialCreateResidenceLease lease))
|
||||
{
|
||||
if (!InitialCreateResidences.CanEnqueue(pending, lease))
|
||||
{
|
||||
accepted = pending.Snapshot;
|
||||
transition = default;
|
||||
return false;
|
||||
}
|
||||
bool acceptedByGate = Entities.TryAcceptDeferredState(
|
||||
update,
|
||||
out _);
|
||||
accepted = pending.Snapshot;
|
||||
transition = default;
|
||||
if (!acceptedByGate)
|
||||
return false;
|
||||
EnqueueDormant(
|
||||
pending,
|
||||
lease,
|
||||
RuntimeInitialCreateContinuationKind.State,
|
||||
RuntimeAcceptedPositionSource.Unknown,
|
||||
new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.State,
|
||||
update.Guid,
|
||||
State: update));
|
||||
return true;
|
||||
}
|
||||
bool applied = Entities.TryApplyState(update, out accepted);
|
||||
transition = default;
|
||||
if (!applied
|
||||
|
|
@ -883,24 +1172,58 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
RuntimeInitialCreateResidenceLease priorInitialResidence = default;
|
||||
bool hadPendingInitialResidence =
|
||||
Entities.TryGetActive(
|
||||
if (TryGetPendingInitialResidence(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord pendingCanonical)
|
||||
&& InitialCreateResidences.TryGetTransaction(
|
||||
pendingCanonical,
|
||||
out priorInitialResidence);
|
||||
if (hadPendingInitialResidence
|
||||
&& !InitialCreateResidences.CanEnqueueAcceptedPosition(
|
||||
pendingCanonical,
|
||||
priorInitialResidence,
|
||||
update))
|
||||
out RuntimeEntityRecord pendingCanonical,
|
||||
out RuntimeInitialCreateResidenceLease pendingLease))
|
||||
{
|
||||
disposition = PositionTimestampDisposition.Rejected;
|
||||
accepted = default;
|
||||
timestamps = default;
|
||||
return false;
|
||||
if (!RuntimeAuthoritativePositionRouteClassifier
|
||||
.IsValidCreateWirePosition(update.Position)
|
||||
|| update.Velocity is { } velocity
|
||||
&& !IsFinite(velocity)
|
||||
|| !InitialCreateResidences.CanEnqueue(
|
||||
pendingCanonical,
|
||||
pendingLease))
|
||||
{
|
||||
disposition = PositionTimestampDisposition.Rejected;
|
||||
accepted = default;
|
||||
timestamps = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool deferredKnown = Entities.TryAcceptDeferredPosition(
|
||||
update,
|
||||
isLocalPlayer,
|
||||
out disposition,
|
||||
out timestamps,
|
||||
out bool timestampMutation);
|
||||
accepted = pendingCanonical.Snapshot;
|
||||
if (!deferredKnown)
|
||||
return false;
|
||||
|
||||
if (disposition is PositionTimestampDisposition.Rejected
|
||||
&& !timestampMutation)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
EnqueueDormant(
|
||||
pendingCanonical,
|
||||
pendingLease,
|
||||
RuntimeInitialCreateContinuationKind.Position,
|
||||
RuntimeAcceptedPositionSource.PositionEvent,
|
||||
new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.Position,
|
||||
update.Guid,
|
||||
Position: update,
|
||||
PositionSource:
|
||||
RuntimeAcceptedPositionSource.PositionEvent,
|
||||
PositionDisposition: disposition,
|
||||
PreviousTeleportSequence:
|
||||
timestamps.PreviousTeleport,
|
||||
AcceptedTimestamps: timestamps,
|
||||
HasTimestampMutation: timestampMutation));
|
||||
return true;
|
||||
}
|
||||
bool hadCanonical = Entities.TryGetActive(
|
||||
update.Guid,
|
||||
|
|
@ -938,43 +1261,6 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
|| projectionRequiresTeleportHook,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Initial CreateObject residence is one immutable transaction. A
|
||||
// Position packet accepted before its 0x11 placement completes is
|
||||
// retained in the transaction's ordered continuation batch; it must
|
||||
// not replace the initial mover operation, mutate the Runtime record
|
||||
// used to prepare that operation, or escape through a host callback.
|
||||
if (hadPendingInitialResidence)
|
||||
{
|
||||
if (!acceptedPosition)
|
||||
return true;
|
||||
if (!ReferenceEquals(canonical, pendingCanonical))
|
||||
throw FailInitialResidenceRegistration(
|
||||
canonical,
|
||||
publishDeleted: true);
|
||||
|
||||
RuntimeInitialCreateResidenceLease retained =
|
||||
InitialCreateResidences.EnqueueAcceptedPosition(
|
||||
canonical,
|
||||
priorInitialResidence,
|
||||
update,
|
||||
accepted,
|
||||
disposition,
|
||||
timestamps,
|
||||
isLocalPlayer,
|
||||
forcePositionRotation,
|
||||
currentLocalVelocity,
|
||||
projectionRequiresTeleportHook);
|
||||
if (!retained.IsValid)
|
||||
{
|
||||
throw FailInitialResidenceRegistration(
|
||||
canonical,
|
||||
publishDeleted: true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
RuntimePlacementCancellationReceipt cancellation = default;
|
||||
if (acceptedPosition)
|
||||
{
|
||||
|
|
@ -1118,6 +1404,15 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
out RuntimeEntityDeleteAcceptance acceptance)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (!isLocalPlayer)
|
||||
{
|
||||
// A child whose complete raw CreateObject is waiting on a missing
|
||||
// parent has no timestamp gate yet. Cancel its exact/older raw
|
||||
// generation before the normal known-object delete gate returns.
|
||||
Entities.ParentAttachments.CancelDeferredChildGeneration(
|
||||
delete.Guid,
|
||||
delete.InstanceSequence);
|
||||
}
|
||||
if (!Entities.TryDelete(delete, isLocalPlayer))
|
||||
{
|
||||
acceptance = null!;
|
||||
|
|
@ -1414,6 +1709,288 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
Entities.IsCurrent(canonical)
|
||||
&& matchesCommittedMutation();
|
||||
|
||||
private bool TryGetPendingInitialResidence(
|
||||
uint guid,
|
||||
out RuntimeEntityRecord canonical,
|
||||
out RuntimeInitialCreateResidenceLease lease)
|
||||
{
|
||||
if (Entities.TryGetActive(guid, out canonical)
|
||||
&& InitialCreateResidences.TryGetTransaction(
|
||||
canonical,
|
||||
out lease))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
canonical = null!;
|
||||
lease = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void EnqueueDormant(
|
||||
RuntimeEntityRecord canonical,
|
||||
in RuntimeInitialCreateResidenceLease prior,
|
||||
RuntimeInitialCreateContinuationKind kind,
|
||||
RuntimeAcceptedPositionSource positionSource,
|
||||
in RuntimeInitialCreateTailAction action)
|
||||
{
|
||||
RuntimeInitialCreateResidenceLease retained = InitialCreateResidences
|
||||
.EnqueueAccepted(
|
||||
canonical,
|
||||
prior,
|
||||
kind,
|
||||
positionSource,
|
||||
ImmutableArray.Create(action));
|
||||
if (!retained.IsValid)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Accepted {kind} for 0x{canonical.ServerGuid:X8}/{canonical.Incarnation} could not be retained by its initial-placement FIFO.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsFinite(System.Numerics.Vector3 value) =>
|
||||
float.IsFinite(value.X)
|
||||
&& float.IsFinite(value.Y)
|
||||
&& float.IsFinite(value.Z);
|
||||
|
||||
private static bool HasConsistentCreateIdentityAndParent(
|
||||
in WorldSession.EntitySpawn incoming)
|
||||
{
|
||||
if (incoming.Guid == 0u)
|
||||
return false;
|
||||
|
||||
bool hasTopParentGuid = incoming.ParentGuid is not null;
|
||||
bool hasTopParentLocation = incoming.ParentLocation is not null;
|
||||
if (hasTopParentGuid != hasTopParentLocation)
|
||||
return false;
|
||||
|
||||
if (incoming.Physics is not { } physics)
|
||||
{
|
||||
// These flattened values are parser projections of PhysicsDesc.
|
||||
// Without that source block, accepting any of them would create
|
||||
// contradictory admission authority before the raw create is
|
||||
// either queued for a parent or assigned a residence lease.
|
||||
return incoming.Position is null
|
||||
&& incoming.SetupTableId is null
|
||||
&& incoming.MotionState is null
|
||||
&& incoming.MotionTableId is null
|
||||
&& incoming.PhysicsState is null
|
||||
&& incoming.ObjScale is null
|
||||
&& incoming.Friction is null
|
||||
&& incoming.Elasticity is null
|
||||
&& incoming.InstanceSequence == 0
|
||||
&& incoming.MovementSequence == 0
|
||||
&& incoming.ServerControlSequence == 0
|
||||
&& incoming.PositionSequence == 0
|
||||
&& !hasTopParentGuid
|
||||
&& incoming.PlacementId is null;
|
||||
}
|
||||
if (physics.Timestamps.Instance != incoming.InstanceSequence
|
||||
|| physics.Timestamps.Position != incoming.PositionSequence
|
||||
|| physics.Timestamps.Movement != incoming.MovementSequence
|
||||
|| physics.Timestamps.ServerControlledMove
|
||||
!= incoming.ServerControlSequence
|
||||
|| physics.Position != incoming.Position)
|
||||
return false;
|
||||
|
||||
PhysicsAttachment? flattenedParent = incoming.ParentGuid is { } parentGuid
|
||||
&& incoming.ParentLocation is { } parentLocation
|
||||
? new PhysicsAttachment(parentGuid, parentLocation)
|
||||
: null;
|
||||
if (flattenedParent != physics.Parent
|
||||
|| incoming.PlacementId != physics.AnimationFrame)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsStructurallyValidDeferredCreate(
|
||||
in WorldSession.EntitySpawn incoming)
|
||||
{
|
||||
if (incoming.Guid == 0u)
|
||||
return false;
|
||||
if (incoming.Physics is not { } physics)
|
||||
return true;
|
||||
if (physics.Parent is null
|
||||
&& physics.Position is { LandblockId: not 0u } position
|
||||
&& !RuntimeAuthoritativePositionRouteClassifier
|
||||
.IsValidCreateWirePosition(position))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (physics.Velocity is { } velocity && !IsFinite(velocity)
|
||||
|| physics.Acceleration is { } acceleration
|
||||
&& !IsFinite(acceleration)
|
||||
|| physics.AngularVelocity is { } angularVelocity
|
||||
&& !IsFinite(angularVelocity)
|
||||
|| physics.Scale is { } scale && !float.IsFinite(scale)
|
||||
|| physics.Friction is { } friction && !float.IsFinite(friction)
|
||||
|| physics.Elasticity is { } elasticity
|
||||
&& !float.IsFinite(elasticity)
|
||||
|| physics.Translucency is { } translucency
|
||||
&& !float.IsFinite(translucency))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool AdmitSameGenerationCreate(
|
||||
RuntimeEntityRecord canonical,
|
||||
in RuntimeInitialCreateResidenceLease prior,
|
||||
in WorldSession.EntitySpawn incoming,
|
||||
in InboundCreateResult admitted,
|
||||
bool isLocalPlayer)
|
||||
{
|
||||
if (!ReferenceEquals(
|
||||
canonical,
|
||||
Entities.TryGetActive(
|
||||
incoming.Guid,
|
||||
out RuntimeEntityRecord current)
|
||||
? current
|
||||
: null)
|
||||
|| canonical.Incarnation != incoming.InstanceSequence
|
||||
|| admitted.Disposition
|
||||
is not CreateObjectTimestampDisposition.ExistingGeneration
|
||||
|| !InitialCreateResidences.CanEnqueue(canonical, prior))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var actions = ImmutableArray.CreateBuilder<
|
||||
RuntimeInitialCreateTailAction>();
|
||||
RuntimeAcceptedPositionSource positionSource =
|
||||
RuntimeAcceptedPositionSource.Unknown;
|
||||
|
||||
if (admitted.SameGenerationEvents is { } events)
|
||||
{
|
||||
actions.Add(new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind
|
||||
.PreTailDescriptionAdaptation,
|
||||
incoming.Guid,
|
||||
Description: events.Description));
|
||||
|
||||
if (Entities.TryAcceptDeferredObjDesc(
|
||||
events.Appearance,
|
||||
out _))
|
||||
{
|
||||
actions.Add(new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.ObjDesc,
|
||||
incoming.Guid,
|
||||
ObjDesc: events.Appearance));
|
||||
}
|
||||
|
||||
if (events.Parent is { } parent)
|
||||
{
|
||||
if (Entities.TryAcceptDeferredCreateParent(
|
||||
parent,
|
||||
out _))
|
||||
{
|
||||
actions.Add(new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.CreateParent,
|
||||
incoming.Guid,
|
||||
CreateParent: parent));
|
||||
}
|
||||
}
|
||||
else if (events.Position is { } position)
|
||||
{
|
||||
if (!RuntimeAuthoritativePositionRouteClassifier
|
||||
.IsValidCreateWirePosition(position.Position)
|
||||
|| position.Velocity is { } velocity
|
||||
&& !IsFinite(velocity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Entities.TryAcceptDeferredPosition(
|
||||
position,
|
||||
isLocalPlayer,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out AcceptedPhysicsTimestamps timestamps,
|
||||
out bool timestampMutation))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (disposition is not PositionTimestampDisposition.Rejected
|
||||
|| timestampMutation)
|
||||
{
|
||||
positionSource = RuntimeAcceptedPositionSource
|
||||
.SameIncarnationCreate;
|
||||
actions.Add(new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.Position,
|
||||
incoming.Guid,
|
||||
Position: position,
|
||||
PositionSource: positionSource,
|
||||
PositionDisposition: disposition,
|
||||
PreviousTeleportSequence:
|
||||
timestamps.PreviousTeleport,
|
||||
AcceptedTimestamps: timestamps,
|
||||
HasTimestampMutation: timestampMutation));
|
||||
}
|
||||
}
|
||||
else if (events.Pickup is { } pickup
|
||||
&& Entities.TryAcceptDeferredPickup(pickup, out _))
|
||||
{
|
||||
actions.Add(new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.Pickup,
|
||||
incoming.Guid,
|
||||
Pickup: pickup));
|
||||
}
|
||||
|
||||
if (events.Movement is { } movement)
|
||||
{
|
||||
bool payloadApplied = Entities.TryAcceptDeferredMotion(
|
||||
movement,
|
||||
out AcceptedPhysicsTimestamps timestamps,
|
||||
out bool timestampMutation);
|
||||
if (payloadApplied || timestampMutation)
|
||||
{
|
||||
actions.Add(new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.Movement,
|
||||
incoming.Guid,
|
||||
Movement: movement,
|
||||
AcceptedTimestamps: timestamps,
|
||||
AppliesMovementPayload: payloadApplied,
|
||||
RetainMovementPayload: true,
|
||||
HasTimestampMutation: timestampMutation));
|
||||
}
|
||||
}
|
||||
|
||||
if (Entities.TryAcceptDeferredState(events.State, out _))
|
||||
{
|
||||
actions.Add(new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.State,
|
||||
incoming.Guid,
|
||||
State: events.State));
|
||||
}
|
||||
if (Entities.TryAcceptDeferredVector(events.Vector, out _))
|
||||
{
|
||||
actions.Add(new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.Vector,
|
||||
incoming.Guid,
|
||||
Vector: events.Vector));
|
||||
}
|
||||
}
|
||||
|
||||
actions.Add(new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.WeenieDescription,
|
||||
incoming.Guid,
|
||||
WeenieDescription: incoming));
|
||||
actions.Add(new RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind.ResidentCellCleanup,
|
||||
incoming.Guid));
|
||||
|
||||
RuntimeInitialCreateResidenceLease retained = InitialCreateResidences
|
||||
.EnqueueAccepted(
|
||||
canonical,
|
||||
prior,
|
||||
RuntimeInitialCreateContinuationKind.SameIncarnationCreate,
|
||||
positionSource,
|
||||
actions.ToImmutable());
|
||||
return retained.IsValid;
|
||||
}
|
||||
|
||||
private void EnsureNotDisposed() =>
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
using System.Collections.Immutable;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.Runtime.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Takes ownership of decoded wire collections retained beyond the inbound
|
||||
/// callback. The protocol records expose read-only views, but those views may
|
||||
/// still wrap parser-owned arrays. Initial-placement admission must therefore
|
||||
/// copy every collection before an asynchronous residence lease can retain it.
|
||||
/// </summary>
|
||||
internal static class RuntimeInitialCreateAdmissionFreezer
|
||||
{
|
||||
internal static WorldSession.EntitySpawn Freeze(
|
||||
in WorldSession.EntitySpawn spawn) => spawn with
|
||||
{
|
||||
AnimPartChanges = spawn.AnimPartChanges.ToImmutableArray(),
|
||||
TextureChanges = spawn.TextureChanges.ToImmutableArray(),
|
||||
SubPalettes = spawn.SubPalettes.ToImmutableArray(),
|
||||
MotionState = Freeze(spawn.MotionState),
|
||||
Physics = Freeze(spawn.Physics),
|
||||
};
|
||||
|
||||
internal static ObjDescEvent.Parsed Freeze(
|
||||
in ObjDescEvent.Parsed update) => update with
|
||||
{
|
||||
ModelData = Freeze(update.ModelData),
|
||||
};
|
||||
|
||||
internal static WorldSession.EntityMotionUpdate Freeze(
|
||||
in WorldSession.EntityMotionUpdate update) => update with
|
||||
{
|
||||
MotionState = Freeze(update.MotionState),
|
||||
};
|
||||
|
||||
internal static RuntimeInitialCreateTailAction Freeze(
|
||||
in RuntimeInitialCreateTailAction action) => action with
|
||||
{
|
||||
Description = Freeze(action.Description),
|
||||
ObjDesc = action.ObjDesc is { } objDesc
|
||||
? Freeze(objDesc)
|
||||
: null,
|
||||
Movement = action.Movement is { } movement
|
||||
? Freeze(movement)
|
||||
: null,
|
||||
WeenieDescription = action.WeenieDescription is { } spawn
|
||||
? Freeze(spawn)
|
||||
: null,
|
||||
};
|
||||
|
||||
private static CreateObject.ModelData Freeze(
|
||||
in CreateObject.ModelData model) => model with
|
||||
{
|
||||
SubPalettes = model.SubPalettes.ToImmutableArray(),
|
||||
TextureChanges = model.TextureChanges.ToImmutableArray(),
|
||||
AnimPartChanges = model.AnimPartChanges.ToImmutableArray(),
|
||||
};
|
||||
|
||||
private static CreateObject.ServerMotionState Freeze(
|
||||
in CreateObject.ServerMotionState motion) => motion with
|
||||
{
|
||||
Commands = motion.Commands?.ToImmutableArray(),
|
||||
};
|
||||
|
||||
private static CreateObject.ServerMotionState? Freeze(
|
||||
CreateObject.ServerMotionState? motion) => motion is { } value
|
||||
? Freeze(value)
|
||||
: null;
|
||||
|
||||
private static PhysicsSpawnData? Freeze(PhysicsSpawnData? physics)
|
||||
{
|
||||
if (physics is not { } value)
|
||||
return null;
|
||||
|
||||
PhysicsMovementData? movement = value.Movement is { } source
|
||||
? source with
|
||||
{
|
||||
RawData = source.RawData.ToArray(),
|
||||
MotionState = Freeze(source.MotionState),
|
||||
}
|
||||
: null;
|
||||
ReadOnlyMemory<PhysicsAttachment>? children = value.Children is { } list
|
||||
? list.ToArray()
|
||||
: null;
|
||||
return value with
|
||||
{
|
||||
Movement = movement,
|
||||
Children = children,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -30,11 +30,14 @@ internal readonly record struct RuntimeInitialCreateResidenceLease(
|
|||
RuntimeInitialCreateResidenceToken Token,
|
||||
RuntimeAuthoritativePositionRoute Route,
|
||||
RuntimeEntityPlacementToken Placement,
|
||||
WorldSession.EntitySpawn InitialCreate,
|
||||
ImmutableArray<RuntimeInitialCreateResidenceContinuation> Continuations)
|
||||
{
|
||||
internal bool IsValid => Token.IsValid
|
||||
&& Route.Accepted
|
||||
&& Route.Authority.Entity == Token.Entity
|
||||
&& InitialCreate.Guid != 0u
|
||||
&& InitialCreate.InstanceSequence == Token.Entity.Incarnation
|
||||
&& !Continuations.IsDefault
|
||||
&& HasValidContinuationChain()
|
||||
&& (!Route.PerformsSetPosition
|
||||
|
|
@ -43,7 +46,7 @@ internal readonly record struct RuntimeInitialCreateResidenceLease(
|
|||
|
||||
private bool HasValidContinuationChain()
|
||||
{
|
||||
ushort previousTeleport = Route.Authority.AcceptedTeleportSequence;
|
||||
uint ownerGuid = InitialCreate.Guid;
|
||||
for (int index = 0; index < Continuations.Length; index++)
|
||||
{
|
||||
RuntimeInitialCreateResidenceContinuation continuation =
|
||||
|
|
@ -51,52 +54,291 @@ internal readonly record struct RuntimeInitialCreateResidenceLease(
|
|||
if (!continuation.IsValid
|
||||
|| continuation.Sequence != (ulong)index + 1UL
|
||||
|| continuation.InstanceSequence != Token.Entity.Incarnation
|
||||
|| continuation.PositionAuthorityVersion
|
||||
!= Token.PositionAuthorityVersion
|
||||
|| continuation.PreviousTeleportSequence
|
||||
!= previousTeleport)
|
||||
|| continuation.Actions.Any(
|
||||
action => action.OwnerGuid != ownerGuid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
previousTeleport = continuation.AcceptedTeleportSequence;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal enum RuntimeInitialCreateContinuationKind : byte
|
||||
{
|
||||
SameIncarnationCreate,
|
||||
ObjDesc,
|
||||
Parent,
|
||||
Pickup,
|
||||
Position,
|
||||
Movement,
|
||||
State,
|
||||
Vector,
|
||||
}
|
||||
|
||||
internal enum RuntimeInitialCreateTailActionKind : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Isolated AP-119 compatibility behavior. Retail's equal-generation
|
||||
/// Create tail starts at ObjDesc and does not call set_description again.
|
||||
/// </summary>
|
||||
PreTailDescriptionAdaptation,
|
||||
ObjDesc,
|
||||
CreateParent,
|
||||
Parent,
|
||||
Pickup,
|
||||
Position,
|
||||
Movement,
|
||||
State,
|
||||
Vector,
|
||||
WeenieDescription,
|
||||
ResidentCellCleanup,
|
||||
}
|
||||
|
||||
internal readonly record struct RuntimeInitialCreateTailAction(
|
||||
RuntimeInitialCreateTailActionKind Kind,
|
||||
uint OwnerGuid,
|
||||
PhysicsSpawnData? Description = null,
|
||||
ObjDescEvent.Parsed? ObjDesc = null,
|
||||
CreateParentUpdate? CreateParent = null,
|
||||
ParentEvent.Parsed? Parent = null,
|
||||
PickupEvent.Parsed? Pickup = null,
|
||||
WorldSession.EntityPositionUpdate? Position = null,
|
||||
WorldSession.EntityMotionUpdate? Movement = null,
|
||||
SetState.Parsed? State = null,
|
||||
VectorUpdate.Parsed? Vector = null,
|
||||
WorldSession.EntitySpawn? WeenieDescription = null,
|
||||
RuntimeAcceptedPositionSource PositionSource =
|
||||
RuntimeAcceptedPositionSource.Unknown,
|
||||
PositionTimestampDisposition PositionDisposition =
|
||||
PositionTimestampDisposition.Rejected,
|
||||
ushort PreviousTeleportSequence = 0,
|
||||
AcceptedPhysicsTimestamps AcceptedTimestamps = default,
|
||||
bool AppliesMovementPayload = false,
|
||||
bool RetainMovementPayload = true,
|
||||
bool HasTimestampMutation = false)
|
||||
{
|
||||
internal bool IsStructurallyValid => HasExclusivePayload()
|
||||
&& Kind switch
|
||||
{
|
||||
RuntimeInitialCreateTailActionKind.PreTailDescriptionAdaptation =>
|
||||
Description is not null,
|
||||
RuntimeInitialCreateTailActionKind.ObjDesc => ObjDesc is { } objDesc
|
||||
&& objDesc.Guid == OwnerGuid,
|
||||
RuntimeInitialCreateTailActionKind.CreateParent =>
|
||||
CreateParent is { } createParent
|
||||
&& createParent.ChildGuid == OwnerGuid,
|
||||
RuntimeInitialCreateTailActionKind.Parent => Parent is { } parent
|
||||
&& parent.ChildGuid == OwnerGuid,
|
||||
RuntimeInitialCreateTailActionKind.Pickup => Pickup is { } pickup
|
||||
&& pickup.Guid == OwnerGuid,
|
||||
RuntimeInitialCreateTailActionKind.Position => Position is { } position
|
||||
&& position.Guid == OwnerGuid
|
||||
&& PositionSource is RuntimeAcceptedPositionSource.PositionEvent
|
||||
or RuntimeAcceptedPositionSource.SameIncarnationCreate
|
||||
&& (PositionDisposition is PositionTimestampDisposition.Apply
|
||||
or PositionTimestampDisposition.ForcePosition
|
||||
|| PositionDisposition is PositionTimestampDisposition.Rejected
|
||||
&& HasTimestampMutation),
|
||||
RuntimeInitialCreateTailActionKind.Movement => Movement is { } movement
|
||||
&& movement.Guid == OwnerGuid
|
||||
&& (AppliesMovementPayload || HasTimestampMutation),
|
||||
RuntimeInitialCreateTailActionKind.State => State is { } state
|
||||
&& state.Guid == OwnerGuid,
|
||||
RuntimeInitialCreateTailActionKind.Vector => Vector is { } vector
|
||||
&& vector.Guid == OwnerGuid,
|
||||
RuntimeInitialCreateTailActionKind.WeenieDescription =>
|
||||
WeenieDescription is { } weenie
|
||||
&& weenie.Guid == OwnerGuid,
|
||||
RuntimeInitialCreateTailActionKind.ResidentCellCleanup => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
internal bool MatchesInstance(ushort instanceSequence) => Kind switch
|
||||
{
|
||||
RuntimeInitialCreateTailActionKind.PreTailDescriptionAdaptation =>
|
||||
Description is { } description
|
||||
&& description.Timestamps.Instance == instanceSequence,
|
||||
RuntimeInitialCreateTailActionKind.ObjDesc =>
|
||||
ObjDesc is { } objDesc
|
||||
&& objDesc.InstanceSequence == instanceSequence,
|
||||
RuntimeInitialCreateTailActionKind.CreateParent =>
|
||||
CreateParent is { } createParent
|
||||
&& createParent.ChildInstanceSequence == instanceSequence,
|
||||
// ParentEvent carries only the parent's INSTANCE_TS. Acceptance
|
||||
// already exact-gated the child's current incarnation; preserve that
|
||||
// proof in AcceptedTimestamps for the later no-regate executor.
|
||||
RuntimeInitialCreateTailActionKind.Parent =>
|
||||
AcceptedTimestamps.Instance == instanceSequence,
|
||||
RuntimeInitialCreateTailActionKind.Pickup =>
|
||||
Pickup is { } pickup
|
||||
&& pickup.InstanceSequence == instanceSequence,
|
||||
RuntimeInitialCreateTailActionKind.Position =>
|
||||
Position is { } position
|
||||
&& position.InstanceSequence == instanceSequence,
|
||||
RuntimeInitialCreateTailActionKind.Movement =>
|
||||
Movement is { } movement
|
||||
&& movement.InstanceSequence == instanceSequence,
|
||||
RuntimeInitialCreateTailActionKind.State =>
|
||||
State is { } state
|
||||
&& state.InstanceSequence == instanceSequence,
|
||||
RuntimeInitialCreateTailActionKind.Vector =>
|
||||
Vector is { } vector
|
||||
&& vector.InstanceSequence == instanceSequence,
|
||||
RuntimeInitialCreateTailActionKind.WeenieDescription =>
|
||||
WeenieDescription is { } weenie
|
||||
&& weenie.InstanceSequence == instanceSequence,
|
||||
RuntimeInitialCreateTailActionKind.ResidentCellCleanup => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private bool HasExclusivePayload()
|
||||
{
|
||||
int payloadCount = (Description is null ? 0 : 1)
|
||||
+ (ObjDesc is null ? 0 : 1)
|
||||
+ (CreateParent is null ? 0 : 1)
|
||||
+ (Parent is null ? 0 : 1)
|
||||
+ (Pickup is null ? 0 : 1)
|
||||
+ (Position is null ? 0 : 1)
|
||||
+ (Movement is null ? 0 : 1)
|
||||
+ (State is null ? 0 : 1)
|
||||
+ (Vector is null ? 0 : 1)
|
||||
+ (WeenieDescription is null ? 0 : 1);
|
||||
return Kind is RuntimeInitialCreateTailActionKind.ResidentCellCleanup
|
||||
? payloadCount == 0
|
||||
: payloadCount == 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One accepted Position packet which arrived before the immutable initial
|
||||
/// CreateObject admission completed. These records remain dormant until a
|
||||
/// host adopts the completed batch; accepting them never replaces or mutates
|
||||
/// the initial 0x11 SetPosition operation.
|
||||
/// One immutable inbound envelope which arrived while retail's synchronous
|
||||
/// CreateObject critical section was virtualized by an asynchronous initial
|
||||
/// SetPosition. Envelopes are never coalesced. A same-incarnation Create is
|
||||
/// one atomic envelope whose ordered tail is expanded only by the executor.
|
||||
/// Position retains the raw PositionPack and is classified only at the FIFO
|
||||
/// head, after every earlier envelope has committed.
|
||||
/// </summary>
|
||||
internal readonly record struct RuntimeInitialCreateResidenceContinuation(
|
||||
ulong Sequence,
|
||||
RuntimePositionEntityKind EntityKind,
|
||||
RuntimeInitialCreateContinuationKind Kind,
|
||||
ushort InstanceSequence,
|
||||
ushort PositionSequence,
|
||||
ushort PreviousTeleportSequence,
|
||||
ushort TeleportSequence,
|
||||
ushort ForcePositionSequence,
|
||||
ushort AcceptedTeleportSequence,
|
||||
ushort AcceptedForcePositionSequence,
|
||||
PositionTimestampDisposition TimestampDisposition,
|
||||
CreateObject.ServerPosition AcceptedWirePosition,
|
||||
Vector3? PositionPackVelocity,
|
||||
Vector3? AcceptedVelocity,
|
||||
Quaternion? ForcePositionRotation,
|
||||
Vector3? CurrentLocalVelocity,
|
||||
bool ProjectionRequiresTeleportHook,
|
||||
uint PlacementFrame,
|
||||
ulong PositionAuthorityVersion)
|
||||
RuntimeAcceptedPositionSource PositionSource,
|
||||
ImmutableArray<RuntimeInitialCreateTailAction> Actions)
|
||||
{
|
||||
internal bool IsValid => Sequence != 0UL
|
||||
&& EntityKind is RuntimePositionEntityKind.LocalPlayer
|
||||
or RuntimePositionEntityKind.Remote
|
||||
or RuntimePositionEntityKind.Projectile
|
||||
&& PositionAuthorityVersion != 0UL
|
||||
&& TimestampDisposition is PositionTimestampDisposition.Apply
|
||||
or PositionTimestampDisposition.ForcePosition;
|
||||
&& !Actions.IsDefaultOrEmpty
|
||||
&& Actions.All(static action => action.IsStructurallyValid)
|
||||
&& HasMatchingInstances()
|
||||
&& HasValidShape();
|
||||
|
||||
private bool HasMatchingInstances()
|
||||
{
|
||||
for (int index = 0; index < Actions.Length; index++)
|
||||
{
|
||||
if (!Actions[index].MatchesInstance(InstanceSequence))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HasValidShape()
|
||||
{
|
||||
if (Kind is not RuntimeInitialCreateContinuationKind.SameIncarnationCreate)
|
||||
{
|
||||
if (Actions.Length != 1)
|
||||
return false;
|
||||
RuntimeInitialCreateTailAction action = Actions[0];
|
||||
RuntimeInitialCreateTailActionKind expected = Kind switch
|
||||
{
|
||||
RuntimeInitialCreateContinuationKind.ObjDesc =>
|
||||
RuntimeInitialCreateTailActionKind.ObjDesc,
|
||||
RuntimeInitialCreateContinuationKind.Parent =>
|
||||
RuntimeInitialCreateTailActionKind.Parent,
|
||||
RuntimeInitialCreateContinuationKind.Pickup =>
|
||||
RuntimeInitialCreateTailActionKind.Pickup,
|
||||
RuntimeInitialCreateContinuationKind.Position =>
|
||||
RuntimeInitialCreateTailActionKind.Position,
|
||||
RuntimeInitialCreateContinuationKind.Movement =>
|
||||
RuntimeInitialCreateTailActionKind.Movement,
|
||||
RuntimeInitialCreateContinuationKind.State =>
|
||||
RuntimeInitialCreateTailActionKind.State,
|
||||
RuntimeInitialCreateContinuationKind.Vector =>
|
||||
RuntimeInitialCreateTailActionKind.Vector,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Unsupported initial-Create continuation kind {Kind}."),
|
||||
};
|
||||
return action.Kind == expected
|
||||
&& (Kind is RuntimeInitialCreateContinuationKind.Position
|
||||
? PositionSource is RuntimeAcceptedPositionSource.PositionEvent
|
||||
&& action.PositionSource == PositionSource
|
||||
: PositionSource is RuntimeAcceptedPositionSource.Unknown);
|
||||
}
|
||||
|
||||
if (Actions.Length < 2
|
||||
|| Actions[^2].Kind
|
||||
is not RuntimeInitialCreateTailActionKind.WeenieDescription
|
||||
|| Actions[^1].Kind
|
||||
is not RuntimeInitialCreateTailActionKind.ResidentCellCleanup)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasPosition = Actions.Any(
|
||||
static action => action.Kind
|
||||
is RuntimeInitialCreateTailActionKind.Position);
|
||||
if (hasPosition
|
||||
? PositionSource
|
||||
is not RuntimeAcceptedPositionSource.SameIncarnationCreate
|
||||
: PositionSource is not RuntimeAcceptedPositionSource.Unknown)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int previousStage = -1;
|
||||
int positionBranchCount = 0;
|
||||
for (int index = 0; index < Actions.Length; index++)
|
||||
{
|
||||
RuntimeInitialCreateTailAction action = Actions[index];
|
||||
int stage = SameCreateStage(action.Kind);
|
||||
if (stage <= previousStage)
|
||||
return false;
|
||||
if (action.Kind is RuntimeInitialCreateTailActionKind.Position
|
||||
&& action.PositionSource != PositionSource)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (action.Kind is RuntimeInitialCreateTailActionKind.CreateParent
|
||||
or RuntimeInitialCreateTailActionKind.Pickup
|
||||
or RuntimeInitialCreateTailActionKind.Position)
|
||||
{
|
||||
positionBranchCount++;
|
||||
}
|
||||
previousStage = stage;
|
||||
}
|
||||
bool hasPhysicsDescription = Actions.Any(
|
||||
static action => action.Kind
|
||||
is RuntimeInitialCreateTailActionKind
|
||||
.PreTailDescriptionAdaptation);
|
||||
return hasPhysicsDescription
|
||||
? positionBranchCount <= 1
|
||||
: positionBranchCount == 0;
|
||||
}
|
||||
|
||||
private static int SameCreateStage(RuntimeInitialCreateTailActionKind kind) =>
|
||||
kind switch
|
||||
{
|
||||
RuntimeInitialCreateTailActionKind.PreTailDescriptionAdaptation => 0,
|
||||
RuntimeInitialCreateTailActionKind.ObjDesc => 1,
|
||||
RuntimeInitialCreateTailActionKind.CreateParent
|
||||
or RuntimeInitialCreateTailActionKind.Pickup
|
||||
or RuntimeInitialCreateTailActionKind.Position => 2,
|
||||
RuntimeInitialCreateTailActionKind.Movement => 3,
|
||||
RuntimeInitialCreateTailActionKind.State => 4,
|
||||
RuntimeInitialCreateTailActionKind.Vector => 5,
|
||||
RuntimeInitialCreateTailActionKind.WeenieDescription => 6,
|
||||
RuntimeInitialCreateTailActionKind.ResidentCellCleanup => 7,
|
||||
_ => int.MinValue,
|
||||
};
|
||||
}
|
||||
|
||||
internal readonly record struct RuntimeInitialCreateResidenceAdoptionToken(
|
||||
|
|
@ -259,125 +501,37 @@ internal sealed class RuntimeInitialCreateResidenceState
|
|||
return Own(record, route);
|
||||
}
|
||||
|
||||
internal RuntimeInitialCreateResidenceLease EnqueueAcceptedPosition(
|
||||
internal RuntimeInitialCreateResidenceLease EnqueueAccepted(
|
||||
RuntimeEntityRecord record,
|
||||
in RuntimeInitialCreateResidenceLease prior,
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
in WorldSession.EntitySpawn accepted,
|
||||
PositionTimestampDisposition disposition,
|
||||
in AcceptedPhysicsTimestamps timestamps,
|
||||
bool isLocalPlayer,
|
||||
Quaternion? forcePositionRotation,
|
||||
Vector3? currentLocalVelocity,
|
||||
bool projectionRequiresTeleportHook)
|
||||
RuntimeInitialCreateContinuationKind kind,
|
||||
RuntimeAcceptedPositionSource positionSource,
|
||||
ImmutableArray<RuntimeInitialCreateTailAction> actions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (!_entities.IsCurrent(record)
|
||||
|| record.Key is not { } key
|
||||
|| update.Guid != record.ServerGuid
|
||||
|| accepted.Guid != record.ServerGuid
|
||||
|| disposition is PositionTimestampDisposition.Rejected)
|
||||
var owned = ImmutableArray.CreateBuilder<
|
||||
RuntimeInitialCreateTailAction>(actions.Length);
|
||||
foreach (RuntimeInitialCreateTailAction action in actions)
|
||||
{
|
||||
return default;
|
||||
owned.Add(RuntimeInitialCreateAdmissionFreezer.Freeze(action));
|
||||
}
|
||||
|
||||
RuntimeInitialCreateResidenceLease current;
|
||||
Entry? active = null;
|
||||
CompletedEntry? completed = null;
|
||||
if (_entries.TryGetValue(key, out active)
|
||||
&& ReferenceEquals(active.Record, record)
|
||||
&& active.Lease.Token == prior.Token
|
||||
&& IsCurrent(active))
|
||||
{
|
||||
current = active.Lease;
|
||||
}
|
||||
else if (_completed.TryGetValue(key, out completed)
|
||||
&& ReferenceEquals(completed.Record, record)
|
||||
&& completed.Lease.Token == prior.Token
|
||||
&& IsCompletedCurrent(completed))
|
||||
{
|
||||
current = completed.Lease;
|
||||
}
|
||||
else
|
||||
{
|
||||
return default;
|
||||
}
|
||||
if (current.Continuations.Length == int.MaxValue
|
||||
|| completed is not null
|
||||
&& completed.Receipt.Adoption.Revision == ulong.MaxValue)
|
||||
return default;
|
||||
|
||||
RuntimePositionEntityKind entityKind = isLocalPlayer
|
||||
? RuntimePositionEntityKind.LocalPlayer
|
||||
: (record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0
|
||||
? RuntimePositionEntityKind.Projectile
|
||||
: RuntimePositionEntityKind.Remote;
|
||||
CreateObject.ServerPosition acceptedPosition =
|
||||
accepted.Position ?? update.Position;
|
||||
ushort previousTeleport = current.Continuations.IsEmpty
|
||||
? current.Route.Authority.AcceptedTeleportSequence
|
||||
: current.Continuations[^1].AcceptedTeleportSequence;
|
||||
ulong sequence = (ulong)current.Continuations.Length + 1UL;
|
||||
var continuation = new RuntimeInitialCreateResidenceContinuation(
|
||||
sequence,
|
||||
entityKind,
|
||||
update.InstanceSequence,
|
||||
update.PositionSequence,
|
||||
previousTeleport,
|
||||
update.TeleportSequence,
|
||||
update.ForcePositionSequence,
|
||||
timestamps.Teleport,
|
||||
timestamps.ForcePosition,
|
||||
disposition,
|
||||
acceptedPosition,
|
||||
update.Velocity,
|
||||
accepted.Physics?.Velocity,
|
||||
forcePositionRotation,
|
||||
currentLocalVelocity,
|
||||
projectionRequiresTeleportHook,
|
||||
accepted.PlacementId ?? 0u,
|
||||
record.PositionAuthorityVersion);
|
||||
if (!continuation.IsValid)
|
||||
return default;
|
||||
|
||||
RuntimeInitialCreateResidenceLease revised = current with
|
||||
{
|
||||
Continuations = current.Continuations.Add(continuation),
|
||||
};
|
||||
if (active is not null)
|
||||
{
|
||||
active.Lease = revised;
|
||||
}
|
||||
else
|
||||
{
|
||||
completed!.Lease = revised;
|
||||
RuntimeInitialCreateResidenceAdoptionToken revisedAdoption =
|
||||
completed.Receipt.Adoption with
|
||||
{
|
||||
Revision = completed.Receipt.Adoption.Revision + 1UL,
|
||||
};
|
||||
completed.Receipt = completed.Receipt with
|
||||
{
|
||||
Adoption = revisedAdoption,
|
||||
Continuations = revised.Continuations,
|
||||
};
|
||||
}
|
||||
return revised;
|
||||
return Enqueue(
|
||||
record,
|
||||
prior,
|
||||
new RuntimeInitialCreateResidenceContinuation(
|
||||
NextSequence(prior),
|
||||
kind,
|
||||
record.Incarnation,
|
||||
positionSource,
|
||||
owned.MoveToImmutable()));
|
||||
}
|
||||
|
||||
internal bool CanEnqueueAcceptedPosition(
|
||||
internal bool CanEnqueue(
|
||||
RuntimeEntityRecord record,
|
||||
in RuntimeInitialCreateResidenceLease prior,
|
||||
in WorldSession.EntityPositionUpdate update)
|
||||
in RuntimeInitialCreateResidenceLease prior)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (record.Key is not { } key
|
||||
|| update.Guid != record.ServerGuid
|
||||
|| !RuntimeAuthoritativePositionRouteClassifier
|
||||
.IsValidCreateWirePosition(update.Position))
|
||||
{
|
||||
if (record.Key is not { } key)
|
||||
return false;
|
||||
}
|
||||
if (_entries.TryGetValue(key, out Entry? entry))
|
||||
{
|
||||
return ReferenceEquals(entry.Record, record)
|
||||
|
|
@ -393,6 +547,61 @@ internal sealed class RuntimeInitialCreateResidenceState
|
|||
&& completed.Receipt.Adoption.Revision < ulong.MaxValue;
|
||||
}
|
||||
|
||||
private RuntimeInitialCreateResidenceLease Enqueue(
|
||||
RuntimeEntityRecord record,
|
||||
in RuntimeInitialCreateResidenceLease prior,
|
||||
in RuntimeInitialCreateResidenceContinuation continuation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (!continuation.IsValid
|
||||
|| continuation.InstanceSequence != record.Incarnation
|
||||
|| !CanEnqueue(record, prior))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
RuntimeEntityKey key = record.Key!.Value;
|
||||
Entry? active = null;
|
||||
CompletedEntry? completed = null;
|
||||
RuntimeInitialCreateResidenceLease current;
|
||||
if (_entries.TryGetValue(key, out active))
|
||||
current = active.Lease;
|
||||
else if (_completed.TryGetValue(key, out completed))
|
||||
current = completed.Lease;
|
||||
else
|
||||
return default;
|
||||
|
||||
if (continuation.Sequence != NextSequence(current))
|
||||
return default;
|
||||
RuntimeInitialCreateResidenceLease revised = current with
|
||||
{
|
||||
Continuations = current.Continuations.Add(continuation),
|
||||
};
|
||||
if (active is not null)
|
||||
{
|
||||
active.Lease = revised;
|
||||
}
|
||||
else
|
||||
{
|
||||
completed!.Lease = revised;
|
||||
RuntimeInitialCreateResidenceAdoptionToken adoption =
|
||||
completed.Receipt.Adoption with
|
||||
{
|
||||
Revision = completed.Receipt.Adoption.Revision + 1UL,
|
||||
};
|
||||
completed.Receipt = completed.Receipt with
|
||||
{
|
||||
Adoption = adoption,
|
||||
Continuations = revised.Continuations,
|
||||
};
|
||||
}
|
||||
return revised;
|
||||
}
|
||||
|
||||
private static ulong NextSequence(
|
||||
in RuntimeInitialCreateResidenceLease lease) =>
|
||||
(ulong)lease.Continuations.Length + 1UL;
|
||||
|
||||
internal bool TryGetTransaction(
|
||||
RuntimeEntityRecord record,
|
||||
out RuntimeInitialCreateResidenceLease lease)
|
||||
|
|
@ -457,6 +666,7 @@ internal sealed class RuntimeInitialCreateResidenceState
|
|||
token,
|
||||
route,
|
||||
placement,
|
||||
RuntimeInitialCreateAdmissionFreezer.Freeze(record.Snapshot),
|
||||
ImmutableArray<RuntimeInitialCreateResidenceContinuation>.Empty);
|
||||
_entries.Add(key, new Entry
|
||||
{
|
||||
|
|
@ -600,6 +810,12 @@ internal sealed class RuntimeInitialCreateResidenceState
|
|||
Retire(current);
|
||||
return false;
|
||||
}
|
||||
// Admission-only checkpoint: a completed initial placement remains
|
||||
// owned until the next slice's continuation executor has drained the
|
||||
// exact FIFO revision. Letting a host acknowledge here would silently
|
||||
// discard accepted packets.
|
||||
if (!current.Lease.Continuations.IsEmpty)
|
||||
return false;
|
||||
if (current.Lease.Route.PerformsSetPosition
|
||||
&& !_setPosition.ConsumeAcknowledgedPlacement(
|
||||
current.Lease.Placement,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,19 @@ internal enum RuntimeTeleportHookPhase : byte
|
|||
AfterEnterWorld,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exact placement of retail's <c>ConstrainTo</c> relative to the selected
|
||||
/// position operation. Local ordinary corrections constrain before their
|
||||
/// optional interpolation; teleports and remote hard moves constrain only
|
||||
/// after the position operation succeeds.
|
||||
/// </summary>
|
||||
internal enum RuntimePositionConstrainPhase : byte
|
||||
{
|
||||
None,
|
||||
BeforePositionOperation,
|
||||
AfterPositionOperation,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exact accepted-wire authority for one presentation-independent position
|
||||
/// route. <see cref="RuntimeEntityKey"/> identifies the incarnation while
|
||||
|
|
@ -147,7 +160,7 @@ internal readonly record struct RuntimeAuthoritativePositionRoute(
|
|||
bool LeaveWorld,
|
||||
RuntimeTeleportHookPhase TeleportHookPhase,
|
||||
bool StopInterpolating,
|
||||
bool ConstrainAfterRouting,
|
||||
RuntimePositionConstrainPhase ConstrainPhase,
|
||||
bool PreserveHeading,
|
||||
bool ZeroVelocity,
|
||||
bool SendPositionImmediately,
|
||||
|
|
@ -163,6 +176,12 @@ internal readonly record struct RuntimeAuthoritativePositionRoute(
|
|||
|
||||
internal bool RunsTeleportHook =>
|
||||
TeleportHookPhase is not RuntimeTeleportHookPhase.None;
|
||||
|
||||
internal bool ConstrainBeforeRouting =>
|
||||
ConstrainPhase is RuntimePositionConstrainPhase.BeforePositionOperation;
|
||||
|
||||
internal bool ConstrainAfterRouting =>
|
||||
ConstrainPhase is RuntimePositionConstrainPhase.AfterPositionOperation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -214,7 +233,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
|||
LeaveWorld: false,
|
||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||
StopInterpolating: false,
|
||||
ConstrainAfterRouting: false,
|
||||
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||
PreserveHeading: false,
|
||||
ZeroVelocity: false,
|
||||
SendPositionImmediately: false,
|
||||
|
|
@ -246,7 +265,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
|||
? RuntimeTeleportHookPhase.AfterEnterWorld
|
||||
: RuntimeTeleportHookPhase.None,
|
||||
StopInterpolating: false,
|
||||
ConstrainAfterRouting: false,
|
||||
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||
PreserveHeading: false,
|
||||
ZeroVelocity: false,
|
||||
SendPositionImmediately: false,
|
||||
|
|
@ -286,7 +305,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
|||
LeaveWorld: false,
|
||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||
StopInterpolating: false,
|
||||
ConstrainAfterRouting: false,
|
||||
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||
PreserveHeading: true,
|
||||
ZeroVelocity: false,
|
||||
SendPositionImmediately: true,
|
||||
|
|
@ -309,7 +328,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
|||
LeaveWorld: false,
|
||||
TeleportHookPhase: RuntimeTeleportHookPhase.AfterPositionOperation,
|
||||
StopInterpolating: false,
|
||||
ConstrainAfterRouting: true,
|
||||
ConstrainPhase: RuntimePositionConstrainPhase.AfterPositionOperation,
|
||||
PreserveHeading: false,
|
||||
ZeroVelocity: true,
|
||||
SendPositionImmediately: false,
|
||||
|
|
@ -331,7 +350,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
|||
LeaveWorld: false,
|
||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||
StopInterpolating: false,
|
||||
ConstrainAfterRouting: true,
|
||||
ConstrainPhase: RuntimePositionConstrainPhase.BeforePositionOperation,
|
||||
PreserveHeading: false,
|
||||
ZeroVelocity: false,
|
||||
SendPositionImmediately: false,
|
||||
|
|
@ -357,7 +376,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
|||
LeaveWorld: false,
|
||||
TeleportHookPhase: RuntimeTeleportHookPhase.BeforePositionOperation,
|
||||
StopInterpolating: false,
|
||||
ConstrainAfterRouting: true,
|
||||
ConstrainPhase: RuntimePositionConstrainPhase.AfterPositionOperation,
|
||||
PreserveHeading: false,
|
||||
ZeroVelocity: false,
|
||||
SendPositionImmediately: false,
|
||||
|
|
@ -383,7 +402,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
|||
LeaveWorld: false,
|
||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||
StopInterpolating: false,
|
||||
ConstrainAfterRouting: false,
|
||||
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||
PreserveHeading: false,
|
||||
ZeroVelocity: false,
|
||||
SendPositionImmediately: false,
|
||||
|
|
@ -413,7 +432,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
|||
LeaveWorld: false,
|
||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||
StopInterpolating: !nearby,
|
||||
ConstrainAfterRouting: true,
|
||||
ConstrainPhase: RuntimePositionConstrainPhase.AfterPositionOperation,
|
||||
PreserveHeading: false,
|
||||
ZeroVelocity: false,
|
||||
SendPositionImmediately: false,
|
||||
|
|
@ -445,7 +464,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
|||
LeaveWorld: true,
|
||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||
StopInterpolating: false,
|
||||
ConstrainAfterRouting: false,
|
||||
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||
PreserveHeading: false,
|
||||
ZeroVelocity: false,
|
||||
SendPositionImmediately: false,
|
||||
|
|
@ -532,7 +551,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
|||
LeaveWorld: false,
|
||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||
StopInterpolating: false,
|
||||
ConstrainAfterRouting: false,
|
||||
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||
PreserveHeading: false,
|
||||
ZeroVelocity: false,
|
||||
SendPositionImmediately: false,
|
||||
|
|
@ -552,7 +571,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
|||
LeaveWorld: false,
|
||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||
StopInterpolating: false,
|
||||
ConstrainAfterRouting: false,
|
||||
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||
PreserveHeading: false,
|
||||
ZeroVelocity: false,
|
||||
SendPositionImmediately: false,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -140,6 +140,7 @@ public sealed class RuntimeAuthoritativePositionRouteClassifierTests
|
|||
playerDistance: distance);
|
||||
|
||||
Assert.Equal(expected, route.Disposition);
|
||||
Assert.False(route.ConstrainBeforeRouting);
|
||||
Assert.True(route.ConstrainAfterRouting);
|
||||
}
|
||||
|
||||
|
|
@ -326,7 +327,8 @@ public sealed class RuntimeAuthoritativePositionRouteClassifierTests
|
|||
: null);
|
||||
|
||||
Assert.Equal(expected, route.Disposition);
|
||||
Assert.True(route.ConstrainAfterRouting);
|
||||
Assert.True(route.ConstrainBeforeRouting);
|
||||
Assert.False(route.ConstrainAfterRouting);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue