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) =>
|
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
|
||||||
_snapshots.TryGetValue(guid, out 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(
|
public CreateObjectTimestampDisposition PreviewCreateDisposition(
|
||||||
WorldSession.EntitySpawn incoming) =>
|
WorldSession.EntitySpawn incoming) =>
|
||||||
_gates.TryGetValue(incoming.Guid, out PhysicsTimestampGate? gate)
|
_gates.TryGetValue(incoming.Guid, out PhysicsTimestampGate? gate)
|
||||||
? gate.PreviewCreateObject(incoming.InstanceSequence)
|
? gate.PreviewCreateObject(incoming.InstanceSequence)
|
||||||
: CreateObjectTimestampDisposition.InitialGeneration;
|
: 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))
|
if (!_gates.TryGetValue(incoming.Guid, out PhysicsTimestampGate? gate))
|
||||||
{
|
{
|
||||||
|
|
@ -56,8 +78,11 @@ public sealed class InboundPhysicsStateController
|
||||||
return new InboundCreateResult(disposition, incoming, null, Current(gate));
|
return new InboundCreateResult(disposition, incoming, null, Current(gate));
|
||||||
}
|
}
|
||||||
|
|
||||||
WorldSession.EntitySpawn merged = MergeUntimestampedCreate(retained, incoming);
|
WorldSession.EntitySpawn merged = deferSameGenerationWeenieDescription
|
||||||
_snapshots[incoming.Guid] = merged;
|
? retained
|
||||||
|
: MergeUntimestampedCreate(retained, incoming);
|
||||||
|
if (!deferSameGenerationWeenieDescription)
|
||||||
|
_snapshots[incoming.Guid] = merged;
|
||||||
return new InboundCreateResult(
|
return new InboundCreateResult(
|
||||||
disposition,
|
disposition,
|
||||||
merged,
|
merged,
|
||||||
|
|
@ -422,6 +447,181 @@ public sealed class InboundPhysicsStateController
|
||||||
return true;
|
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>
|
/// <summary>
|
||||||
/// F751 is a notification gate only. Retail compares it to TELEPORT_TS but
|
/// F751 is a notification gate only. Retail compares it to TELEPORT_TS but
|
||||||
/// advances that timestamp later, with the accepted Position packet.
|
/// advances that timestamp later, with the accepted Position packet.
|
||||||
|
|
@ -465,12 +665,15 @@ public sealed class InboundPhysicsStateController
|
||||||
|
|
||||||
private static AcceptedPhysicsTimestamps Current(
|
private static AcceptedPhysicsTimestamps Current(
|
||||||
PhysicsTimestampGate gate,
|
PhysicsTimestampGate gate,
|
||||||
bool teleportAdvanced = false) => new(
|
bool teleportAdvanced = false,
|
||||||
|
ushort previousTeleport = 0) => new(
|
||||||
gate.InstanceTimestamp,
|
gate.InstanceTimestamp,
|
||||||
gate.ServerControlledMoveTimestamp,
|
gate.ServerControlledMoveTimestamp,
|
||||||
gate.TeleportTimestamp,
|
gate.TeleportTimestamp,
|
||||||
gate.ForcePositionTimestamp,
|
gate.ForcePositionTimestamp,
|
||||||
teleportAdvanced);
|
teleportAdvanced,
|
||||||
|
TeleportHookRequired: false,
|
||||||
|
previousTeleport);
|
||||||
|
|
||||||
private static WorldSession.EntitySpawn MirrorGateTimestamps(
|
private static WorldSession.EntitySpawn MirrorGateTimestamps(
|
||||||
WorldSession.EntitySpawn spawn,
|
WorldSession.EntitySpawn spawn,
|
||||||
|
|
@ -671,7 +874,8 @@ public readonly record struct AcceptedPhysicsTimestamps(
|
||||||
ushort Teleport,
|
ushort Teleport,
|
||||||
ushort ForcePosition,
|
ushort ForcePosition,
|
||||||
bool TeleportAdvanced = false,
|
bool TeleportAdvanced = false,
|
||||||
bool TeleportHookRequired = false);
|
bool TeleportHookRequired = false,
|
||||||
|
ushort PreviousTeleport = 0);
|
||||||
|
|
||||||
public readonly record struct CreateParentUpdate(
|
public readonly record struct CreateParentUpdate(
|
||||||
uint ChildGuid,
|
uint ChildGuid,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
|
|
@ -17,12 +18,119 @@ public sealed class ParentAttachmentState
|
||||||
private readonly Dictionary<uint, ParentAttachmentRelation> _recoveryByChild = new();
|
private readonly Dictionary<uint, ParentAttachmentRelation> _recoveryByChild = new();
|
||||||
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = new();
|
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = new();
|
||||||
private readonly Dictionary<ParentIncarnation, List<uint>> _committedChildrenByParent = new();
|
private readonly Dictionary<ParentIncarnation, List<uint>> _committedChildrenByParent = new();
|
||||||
|
private readonly Dictionary<uint, Queue<DeferredParentCreate>>
|
||||||
|
_deferredCreatesByParent = [];
|
||||||
|
private ulong _nextDeferredCreateAdmissionId;
|
||||||
|
|
||||||
public int UnresolvedRelationCount =>
|
public int UnresolvedRelationCount =>
|
||||||
_unresolvedByChild.Values.Sum(queue => queue.Count);
|
_unresolvedByChild.Values.Sum(queue => queue.Count);
|
||||||
public int StagedRelationCount => _stagedByChild.Count;
|
public int StagedRelationCount => _stagedByChild.Count;
|
||||||
public int RecoveryRelationCount => _recoveryByChild.Count;
|
public int RecoveryRelationCount => _recoveryByChild.Count;
|
||||||
public int CommittedRelationCount => _lastAcceptedByChild.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)
|
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
|
||||||
{
|
{
|
||||||
|
|
@ -270,6 +378,7 @@ public sealed class ParentAttachmentState
|
||||||
|
|
||||||
public void RemoveObject(uint guid)
|
public void RemoveObject(uint guid)
|
||||||
{
|
{
|
||||||
|
RemoveDeferredChildCreates(guid);
|
||||||
_stagedByChild.Remove(guid);
|
_stagedByChild.Remove(guid);
|
||||||
_recoveryByChild.Remove(guid);
|
_recoveryByChild.Remove(guid);
|
||||||
RemoveCommittedChild(guid);
|
RemoveCommittedChild(guid);
|
||||||
|
|
@ -298,6 +407,12 @@ public sealed class ParentAttachmentState
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void EndGeneration(uint guid, ushort replacementGeneration)
|
public void EndGeneration(uint guid, ushort replacementGeneration)
|
||||||
{
|
{
|
||||||
|
FilterDeferredCreates(candidate =>
|
||||||
|
candidate.Spawn.Guid != guid
|
||||||
|
|| candidate.Spawn.InstanceSequence == replacementGeneration
|
||||||
|
|| PhysicsTimestampGate.IsNewer(
|
||||||
|
replacementGeneration,
|
||||||
|
candidate.Spawn.InstanceSequence));
|
||||||
FilterChildCandidates(
|
FilterChildCandidates(
|
||||||
guid,
|
guid,
|
||||||
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
||||||
|
|
@ -322,6 +437,7 @@ public sealed class ParentAttachmentState
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void DeleteGeneration(uint guid, ushort deletedGeneration)
|
public void DeleteGeneration(uint guid, ushort deletedGeneration)
|
||||||
{
|
{
|
||||||
|
CancelDeferredChildGeneration(guid, deletedGeneration);
|
||||||
FilterChildCandidates(
|
FilterChildCandidates(
|
||||||
guid,
|
guid,
|
||||||
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
||||||
|
|
@ -351,6 +467,7 @@ public sealed class ParentAttachmentState
|
||||||
|
|
||||||
public void RemoveChild(uint childGuid)
|
public void RemoveChild(uint childGuid)
|
||||||
{
|
{
|
||||||
|
RemoveDeferredChildCreates(childGuid);
|
||||||
_stagedByChild.Remove(childGuid);
|
_stagedByChild.Remove(childGuid);
|
||||||
_recoveryByChild.Remove(childGuid);
|
_recoveryByChild.Remove(childGuid);
|
||||||
RemoveCommittedChild(childGuid);
|
RemoveCommittedChild(childGuid);
|
||||||
|
|
@ -359,6 +476,7 @@ public sealed class ParentAttachmentState
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
|
_deferredCreatesByParent.Clear();
|
||||||
_unresolvedByChild.Clear();
|
_unresolvedByChild.Clear();
|
||||||
_stagedByChild.Clear();
|
_stagedByChild.Clear();
|
||||||
_recoveryByChild.Clear();
|
_recoveryByChild.Clear();
|
||||||
|
|
@ -368,6 +486,26 @@ public sealed class ParentAttachmentState
|
||||||
_committedChildrenByParent.Clear();
|
_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)
|
private void RemoveCommittedChild(uint childGuid)
|
||||||
{
|
{
|
||||||
if (!_lastAcceptedByChild.Remove(
|
if (!_lastAcceptedByChild.Remove(
|
||||||
|
|
@ -462,6 +600,15 @@ public sealed class ParentAttachmentState
|
||||||
ushort InstanceSequence);
|
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(
|
public readonly record struct ParentAttachmentRelation(
|
||||||
uint ParentGuid,
|
uint ParentGuid,
|
||||||
uint ChildGuid,
|
uint ChildGuid,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Runtime.Physics;
|
using AcDream.Runtime.Physics;
|
||||||
|
|
||||||
|
|
@ -47,6 +48,10 @@ public sealed class RuntimeEntityDirectory
|
||||||
public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming) =>
|
public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming) =>
|
||||||
_inbound.AcceptCreate(incoming);
|
_inbound.AcceptCreate(incoming);
|
||||||
|
|
||||||
|
internal InboundCreateResult AcceptCreateDeferredSameGeneration(
|
||||||
|
WorldSession.EntitySpawn incoming) =>
|
||||||
|
_inbound.AcceptCreateDeferredSameGeneration(incoming);
|
||||||
|
|
||||||
public CreateObjectTimestampDisposition PreviewCreateDisposition(
|
public CreateObjectTimestampDisposition PreviewCreateDisposition(
|
||||||
WorldSession.EntitySpawn incoming) =>
|
WorldSession.EntitySpawn incoming) =>
|
||||||
_inbound.PreviewCreateDisposition(incoming);
|
_inbound.PreviewCreateDisposition(incoming);
|
||||||
|
|
@ -59,6 +64,11 @@ public sealed class RuntimeEntityDirectory
|
||||||
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
|
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
|
||||||
_inbound.TryGetSnapshot(guid, out 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) =>
|
public bool TryGetActive(uint guid, out RuntimeEntityRecord record) =>
|
||||||
_activeByGuid.TryGetValue(guid, out record!);
|
_activeByGuid.TryGetValue(guid, out record!);
|
||||||
|
|
||||||
|
|
@ -482,6 +492,58 @@ public sealed class RuntimeEntityDirectory
|
||||||
out accepted,
|
out accepted,
|
||||||
out timestamps);
|
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) =>
|
public bool IsFreshTeleportStart(uint guid, ushort teleportSequence) =>
|
||||||
_inbound.IsFreshTeleportStart(guid, teleportSequence);
|
_inbound.IsFreshTeleportStart(guid, teleportSequence);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
|
|
@ -11,7 +12,8 @@ public readonly record struct RuntimeEntityRegistrationResult(
|
||||||
RuntimeEntityRecord? Canonical,
|
RuntimeEntityRecord? Canonical,
|
||||||
bool LogicalRegistrationCreated,
|
bool LogicalRegistrationCreated,
|
||||||
bool ReplacedExistingGeneration,
|
bool ReplacedExistingGeneration,
|
||||||
Exception? PriorGenerationCleanupFailure = null);
|
Exception? PriorGenerationCleanupFailure = null,
|
||||||
|
bool DeferredForParent = false);
|
||||||
|
|
||||||
public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
||||||
int ActiveEntityCount,
|
int ActiveEntityCount,
|
||||||
|
|
@ -19,6 +21,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
||||||
int ClaimedLocalIdCount,
|
int ClaimedLocalIdCount,
|
||||||
int AcceptedSnapshotCount,
|
int AcceptedSnapshotCount,
|
||||||
int UnresolvedParentRelationCount,
|
int UnresolvedParentRelationCount,
|
||||||
|
int DeferredParentCreateCount,
|
||||||
int StagedParentRelationCount,
|
int StagedParentRelationCount,
|
||||||
int RecoveryParentRelationCount,
|
int RecoveryParentRelationCount,
|
||||||
int CommittedParentRelationCount,
|
int CommittedParentRelationCount,
|
||||||
|
|
@ -44,6 +47,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
||||||
&& ClaimedLocalIdCount == 0
|
&& ClaimedLocalIdCount == 0
|
||||||
&& AcceptedSnapshotCount == 0
|
&& AcceptedSnapshotCount == 0
|
||||||
&& UnresolvedParentRelationCount == 0
|
&& UnresolvedParentRelationCount == 0
|
||||||
|
&& DeferredParentCreateCount == 0
|
||||||
&& StagedParentRelationCount == 0
|
&& StagedParentRelationCount == 0
|
||||||
&& RecoveryParentRelationCount == 0
|
&& RecoveryParentRelationCount == 0
|
||||||
&& CommittedParentRelationCount == 0
|
&& CommittedParentRelationCount == 0
|
||||||
|
|
@ -205,6 +209,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
Entities.ClaimedLocalIdCount,
|
Entities.ClaimedLocalIdCount,
|
||||||
Entities.Snapshots.Count,
|
Entities.Snapshots.Count,
|
||||||
parents.UnresolvedRelationCount,
|
parents.UnresolvedRelationCount,
|
||||||
|
parents.DeferredCreateCount,
|
||||||
parents.StagedRelationCount,
|
parents.StagedRelationCount,
|
||||||
parents.RecoveryRelationCount,
|
parents.RecoveryRelationCount,
|
||||||
parents.CommittedRelationCount,
|
parents.CommittedRelationCount,
|
||||||
|
|
@ -272,6 +277,35 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"A Runtime entity cannot register while its session lifetime is clearing.");
|
"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 =
|
CreateObjectTimestampDisposition preview =
|
||||||
Entities.PreviewCreateDisposition(incoming);
|
Entities.PreviewCreateDisposition(incoming);
|
||||||
bool requiresFreshResidenceAdmission = preview is
|
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.");
|
$"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
|
if (result.Disposition
|
||||||
is CreateObjectTimestampDisposition.StaleGeneration)
|
is CreateObjectTimestampDisposition.StaleGeneration)
|
||||||
{
|
{
|
||||||
|
|
@ -307,6 +378,33 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
incoming.Guid,
|
incoming.Guid,
|
||||||
out RuntimeEntityRecord retained))
|
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
|
// Existing-generation CreateObject contributes untimestamped
|
||||||
// description fields here. Position/Parent/Pickup/State/etc.
|
// description fields here. Position/Parent/Pickup/State/etc.
|
||||||
// remain separate freshness-gated events and must not churn a
|
// remain separate freshness-gated events and must not churn a
|
||||||
|
|
@ -592,6 +690,33 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
out WorldSession.EntitySpawn accepted)
|
out WorldSession.EntitySpawn accepted)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
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);
|
bool applied = Entities.TryApplyObjDesc(update, out accepted);
|
||||||
if (!applied
|
if (!applied
|
||||||
|| !Entities.TryGetActive(
|
|| !Entities.TryGetActive(
|
||||||
|
|
@ -617,6 +742,33 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
out WorldSession.EntitySpawn accepted)
|
out WorldSession.EntitySpawn accepted)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
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);
|
bool applied = Entities.TryApplyPickup(update, out accepted);
|
||||||
if (!applied
|
if (!applied
|
||||||
|| !Entities.TryGetActive(
|
|| !Entities.TryGetActive(
|
||||||
|
|
@ -655,6 +807,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
out WorldSession.EntitySpawn accepted)
|
out WorldSession.EntitySpawn accepted)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
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);
|
bool applied = Entities.TryApplyCreateParent(update, out accepted);
|
||||||
return CommitPositionChannelUpdate(
|
return CommitPositionChannelUpdate(
|
||||||
applied,
|
applied,
|
||||||
|
|
@ -669,6 +829,44 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
out WorldSession.EntitySpawn accepted)
|
out WorldSession.EntitySpawn accepted)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
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);
|
bool applied = Entities.TryApplyParent(update, out accepted);
|
||||||
return CommitPositionChannelUpdate(
|
return CommitPositionChannelUpdate(
|
||||||
applied,
|
applied,
|
||||||
|
|
@ -750,6 +948,39 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
out AcceptedPhysicsTimestamps timestamps)
|
out AcceptedPhysicsTimestamps timestamps)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
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(
|
bool applied = Entities.TryApplyMotion(
|
||||||
update,
|
update,
|
||||||
retainPayload,
|
retainPayload,
|
||||||
|
|
@ -790,6 +1021,35 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
out WorldSession.EntitySpawn accepted)
|
out WorldSession.EntitySpawn accepted)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
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);
|
bool applied = Entities.TryApplyVector(update, out accepted);
|
||||||
if (!applied
|
if (!applied
|
||||||
|| !Entities.TryGetActive(
|
|| !Entities.TryGetActive(
|
||||||
|
|
@ -817,6 +1077,35 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
out RetailPhysicsStateTransition transition)
|
out RetailPhysicsStateTransition transition)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
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);
|
bool applied = Entities.TryApplyState(update, out accepted);
|
||||||
transition = default;
|
transition = default;
|
||||||
if (!applied
|
if (!applied
|
||||||
|
|
@ -883,24 +1172,58 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
out AcceptedPhysicsTimestamps timestamps)
|
out AcceptedPhysicsTimestamps timestamps)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
RuntimeInitialCreateResidenceLease priorInitialResidence = default;
|
if (TryGetPendingInitialResidence(
|
||||||
bool hadPendingInitialResidence =
|
|
||||||
Entities.TryGetActive(
|
|
||||||
update.Guid,
|
update.Guid,
|
||||||
out RuntimeEntityRecord pendingCanonical)
|
out RuntimeEntityRecord pendingCanonical,
|
||||||
&& InitialCreateResidences.TryGetTransaction(
|
out RuntimeInitialCreateResidenceLease pendingLease))
|
||||||
pendingCanonical,
|
|
||||||
out priorInitialResidence);
|
|
||||||
if (hadPendingInitialResidence
|
|
||||||
&& !InitialCreateResidences.CanEnqueueAcceptedPosition(
|
|
||||||
pendingCanonical,
|
|
||||||
priorInitialResidence,
|
|
||||||
update))
|
|
||||||
{
|
{
|
||||||
disposition = PositionTimestampDisposition.Rejected;
|
if (!RuntimeAuthoritativePositionRouteClassifier
|
||||||
accepted = default;
|
.IsValidCreateWirePosition(update.Position)
|
||||||
timestamps = default;
|
|| update.Velocity is { } velocity
|
||||||
return false;
|
&& !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(
|
bool hadCanonical = Entities.TryGetActive(
|
||||||
update.Guid,
|
update.Guid,
|
||||||
|
|
@ -938,43 +1261,6 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
|| projectionRequiresTeleportHook,
|
|| 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;
|
RuntimePlacementCancellationReceipt cancellation = default;
|
||||||
if (acceptedPosition)
|
if (acceptedPosition)
|
||||||
{
|
{
|
||||||
|
|
@ -1118,6 +1404,15 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
out RuntimeEntityDeleteAcceptance acceptance)
|
out RuntimeEntityDeleteAcceptance acceptance)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
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))
|
if (!Entities.TryDelete(delete, isLocalPlayer))
|
||||||
{
|
{
|
||||||
acceptance = null!;
|
acceptance = null!;
|
||||||
|
|
@ -1414,6 +1709,288 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
Entities.IsCurrent(canonical)
|
Entities.IsCurrent(canonical)
|
||||||
&& matchesCommittedMutation();
|
&& 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() =>
|
private void EnsureNotDisposed() =>
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
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,
|
RuntimeInitialCreateResidenceToken Token,
|
||||||
RuntimeAuthoritativePositionRoute Route,
|
RuntimeAuthoritativePositionRoute Route,
|
||||||
RuntimeEntityPlacementToken Placement,
|
RuntimeEntityPlacementToken Placement,
|
||||||
|
WorldSession.EntitySpawn InitialCreate,
|
||||||
ImmutableArray<RuntimeInitialCreateResidenceContinuation> Continuations)
|
ImmutableArray<RuntimeInitialCreateResidenceContinuation> Continuations)
|
||||||
{
|
{
|
||||||
internal bool IsValid => Token.IsValid
|
internal bool IsValid => Token.IsValid
|
||||||
&& Route.Accepted
|
&& Route.Accepted
|
||||||
&& Route.Authority.Entity == Token.Entity
|
&& Route.Authority.Entity == Token.Entity
|
||||||
|
&& InitialCreate.Guid != 0u
|
||||||
|
&& InitialCreate.InstanceSequence == Token.Entity.Incarnation
|
||||||
&& !Continuations.IsDefault
|
&& !Continuations.IsDefault
|
||||||
&& HasValidContinuationChain()
|
&& HasValidContinuationChain()
|
||||||
&& (!Route.PerformsSetPosition
|
&& (!Route.PerformsSetPosition
|
||||||
|
|
@ -43,7 +46,7 @@ internal readonly record struct RuntimeInitialCreateResidenceLease(
|
||||||
|
|
||||||
private bool HasValidContinuationChain()
|
private bool HasValidContinuationChain()
|
||||||
{
|
{
|
||||||
ushort previousTeleport = Route.Authority.AcceptedTeleportSequence;
|
uint ownerGuid = InitialCreate.Guid;
|
||||||
for (int index = 0; index < Continuations.Length; index++)
|
for (int index = 0; index < Continuations.Length; index++)
|
||||||
{
|
{
|
||||||
RuntimeInitialCreateResidenceContinuation continuation =
|
RuntimeInitialCreateResidenceContinuation continuation =
|
||||||
|
|
@ -51,52 +54,291 @@ internal readonly record struct RuntimeInitialCreateResidenceLease(
|
||||||
if (!continuation.IsValid
|
if (!continuation.IsValid
|
||||||
|| continuation.Sequence != (ulong)index + 1UL
|
|| continuation.Sequence != (ulong)index + 1UL
|
||||||
|| continuation.InstanceSequence != Token.Entity.Incarnation
|
|| continuation.InstanceSequence != Token.Entity.Incarnation
|
||||||
|| continuation.PositionAuthorityVersion
|
|| continuation.Actions.Any(
|
||||||
!= Token.PositionAuthorityVersion
|
action => action.OwnerGuid != ownerGuid))
|
||||||
|| continuation.PreviousTeleportSequence
|
|
||||||
!= previousTeleport)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
previousTeleport = continuation.AcceptedTeleportSequence;
|
|
||||||
}
|
}
|
||||||
return true;
|
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>
|
/// <summary>
|
||||||
/// One accepted Position packet which arrived before the immutable initial
|
/// One immutable inbound envelope which arrived while retail's synchronous
|
||||||
/// CreateObject admission completed. These records remain dormant until a
|
/// CreateObject critical section was virtualized by an asynchronous initial
|
||||||
/// host adopts the completed batch; accepting them never replaces or mutates
|
/// SetPosition. Envelopes are never coalesced. A same-incarnation Create is
|
||||||
/// the initial 0x11 SetPosition operation.
|
/// 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>
|
/// </summary>
|
||||||
internal readonly record struct RuntimeInitialCreateResidenceContinuation(
|
internal readonly record struct RuntimeInitialCreateResidenceContinuation(
|
||||||
ulong Sequence,
|
ulong Sequence,
|
||||||
RuntimePositionEntityKind EntityKind,
|
RuntimeInitialCreateContinuationKind Kind,
|
||||||
ushort InstanceSequence,
|
ushort InstanceSequence,
|
||||||
ushort PositionSequence,
|
RuntimeAcceptedPositionSource PositionSource,
|
||||||
ushort PreviousTeleportSequence,
|
ImmutableArray<RuntimeInitialCreateTailAction> Actions)
|
||||||
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
|
internal bool IsValid => Sequence != 0UL
|
||||||
&& EntityKind is RuntimePositionEntityKind.LocalPlayer
|
&& !Actions.IsDefaultOrEmpty
|
||||||
or RuntimePositionEntityKind.Remote
|
&& Actions.All(static action => action.IsStructurallyValid)
|
||||||
or RuntimePositionEntityKind.Projectile
|
&& HasMatchingInstances()
|
||||||
&& PositionAuthorityVersion != 0UL
|
&& HasValidShape();
|
||||||
&& TimestampDisposition is PositionTimestampDisposition.Apply
|
|
||||||
or PositionTimestampDisposition.ForcePosition;
|
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(
|
internal readonly record struct RuntimeInitialCreateResidenceAdoptionToken(
|
||||||
|
|
@ -259,125 +501,37 @@ internal sealed class RuntimeInitialCreateResidenceState
|
||||||
return Own(record, route);
|
return Own(record, route);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal RuntimeInitialCreateResidenceLease EnqueueAcceptedPosition(
|
internal RuntimeInitialCreateResidenceLease EnqueueAccepted(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
in RuntimeInitialCreateResidenceLease prior,
|
in RuntimeInitialCreateResidenceLease prior,
|
||||||
WorldSession.EntityPositionUpdate update,
|
RuntimeInitialCreateContinuationKind kind,
|
||||||
in WorldSession.EntitySpawn accepted,
|
RuntimeAcceptedPositionSource positionSource,
|
||||||
PositionTimestampDisposition disposition,
|
ImmutableArray<RuntimeInitialCreateTailAction> actions)
|
||||||
in AcceptedPhysicsTimestamps timestamps,
|
|
||||||
bool isLocalPlayer,
|
|
||||||
Quaternion? forcePositionRotation,
|
|
||||||
Vector3? currentLocalVelocity,
|
|
||||||
bool projectionRequiresTeleportHook)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
var owned = ImmutableArray.CreateBuilder<
|
||||||
if (!_entities.IsCurrent(record)
|
RuntimeInitialCreateTailAction>(actions.Length);
|
||||||
|| record.Key is not { } key
|
foreach (RuntimeInitialCreateTailAction action in actions)
|
||||||
|| update.Guid != record.ServerGuid
|
|
||||||
|| accepted.Guid != record.ServerGuid
|
|
||||||
|| disposition is PositionTimestampDisposition.Rejected)
|
|
||||||
{
|
{
|
||||||
return default;
|
owned.Add(RuntimeInitialCreateAdmissionFreezer.Freeze(action));
|
||||||
}
|
}
|
||||||
|
return Enqueue(
|
||||||
RuntimeInitialCreateResidenceLease current;
|
record,
|
||||||
Entry? active = null;
|
prior,
|
||||||
CompletedEntry? completed = null;
|
new RuntimeInitialCreateResidenceContinuation(
|
||||||
if (_entries.TryGetValue(key, out active)
|
NextSequence(prior),
|
||||||
&& ReferenceEquals(active.Record, record)
|
kind,
|
||||||
&& active.Lease.Token == prior.Token
|
record.Incarnation,
|
||||||
&& IsCurrent(active))
|
positionSource,
|
||||||
{
|
owned.MoveToImmutable()));
|
||||||
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(
|
internal bool CanEnqueue(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
in RuntimeInitialCreateResidenceLease prior,
|
in RuntimeInitialCreateResidenceLease prior)
|
||||||
in WorldSession.EntityPositionUpdate update)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
if (record.Key is not { } key
|
if (record.Key is not { } key)
|
||||||
|| update.Guid != record.ServerGuid
|
|
||||||
|| !RuntimeAuthoritativePositionRouteClassifier
|
|
||||||
.IsValidCreateWirePosition(update.Position))
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
if (_entries.TryGetValue(key, out Entry? entry))
|
if (_entries.TryGetValue(key, out Entry? entry))
|
||||||
{
|
{
|
||||||
return ReferenceEquals(entry.Record, record)
|
return ReferenceEquals(entry.Record, record)
|
||||||
|
|
@ -393,6 +547,61 @@ internal sealed class RuntimeInitialCreateResidenceState
|
||||||
&& completed.Receipt.Adoption.Revision < ulong.MaxValue;
|
&& 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(
|
internal bool TryGetTransaction(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
out RuntimeInitialCreateResidenceLease lease)
|
out RuntimeInitialCreateResidenceLease lease)
|
||||||
|
|
@ -457,6 +666,7 @@ internal sealed class RuntimeInitialCreateResidenceState
|
||||||
token,
|
token,
|
||||||
route,
|
route,
|
||||||
placement,
|
placement,
|
||||||
|
RuntimeInitialCreateAdmissionFreezer.Freeze(record.Snapshot),
|
||||||
ImmutableArray<RuntimeInitialCreateResidenceContinuation>.Empty);
|
ImmutableArray<RuntimeInitialCreateResidenceContinuation>.Empty);
|
||||||
_entries.Add(key, new Entry
|
_entries.Add(key, new Entry
|
||||||
{
|
{
|
||||||
|
|
@ -600,6 +810,12 @@ internal sealed class RuntimeInitialCreateResidenceState
|
||||||
Retire(current);
|
Retire(current);
|
||||||
return false;
|
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
|
if (current.Lease.Route.PerformsSetPosition
|
||||||
&& !_setPosition.ConsumeAcknowledgedPlacement(
|
&& !_setPosition.ConsumeAcknowledgedPlacement(
|
||||||
current.Lease.Placement,
|
current.Lease.Placement,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,19 @@ internal enum RuntimeTeleportHookPhase : byte
|
||||||
AfterEnterWorld,
|
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>
|
/// <summary>
|
||||||
/// Exact accepted-wire authority for one presentation-independent position
|
/// Exact accepted-wire authority for one presentation-independent position
|
||||||
/// route. <see cref="RuntimeEntityKey"/> identifies the incarnation while
|
/// route. <see cref="RuntimeEntityKey"/> identifies the incarnation while
|
||||||
|
|
@ -147,7 +160,7 @@ internal readonly record struct RuntimeAuthoritativePositionRoute(
|
||||||
bool LeaveWorld,
|
bool LeaveWorld,
|
||||||
RuntimeTeleportHookPhase TeleportHookPhase,
|
RuntimeTeleportHookPhase TeleportHookPhase,
|
||||||
bool StopInterpolating,
|
bool StopInterpolating,
|
||||||
bool ConstrainAfterRouting,
|
RuntimePositionConstrainPhase ConstrainPhase,
|
||||||
bool PreserveHeading,
|
bool PreserveHeading,
|
||||||
bool ZeroVelocity,
|
bool ZeroVelocity,
|
||||||
bool SendPositionImmediately,
|
bool SendPositionImmediately,
|
||||||
|
|
@ -163,6 +176,12 @@ internal readonly record struct RuntimeAuthoritativePositionRoute(
|
||||||
|
|
||||||
internal bool RunsTeleportHook =>
|
internal bool RunsTeleportHook =>
|
||||||
TeleportHookPhase is not RuntimeTeleportHookPhase.None;
|
TeleportHookPhase is not RuntimeTeleportHookPhase.None;
|
||||||
|
|
||||||
|
internal bool ConstrainBeforeRouting =>
|
||||||
|
ConstrainPhase is RuntimePositionConstrainPhase.BeforePositionOperation;
|
||||||
|
|
||||||
|
internal bool ConstrainAfterRouting =>
|
||||||
|
ConstrainPhase is RuntimePositionConstrainPhase.AfterPositionOperation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -214,7 +233,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
||||||
LeaveWorld: false,
|
LeaveWorld: false,
|
||||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||||
StopInterpolating: false,
|
StopInterpolating: false,
|
||||||
ConstrainAfterRouting: false,
|
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||||
PreserveHeading: false,
|
PreserveHeading: false,
|
||||||
ZeroVelocity: false,
|
ZeroVelocity: false,
|
||||||
SendPositionImmediately: false,
|
SendPositionImmediately: false,
|
||||||
|
|
@ -246,7 +265,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
||||||
? RuntimeTeleportHookPhase.AfterEnterWorld
|
? RuntimeTeleportHookPhase.AfterEnterWorld
|
||||||
: RuntimeTeleportHookPhase.None,
|
: RuntimeTeleportHookPhase.None,
|
||||||
StopInterpolating: false,
|
StopInterpolating: false,
|
||||||
ConstrainAfterRouting: false,
|
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||||
PreserveHeading: false,
|
PreserveHeading: false,
|
||||||
ZeroVelocity: false,
|
ZeroVelocity: false,
|
||||||
SendPositionImmediately: false,
|
SendPositionImmediately: false,
|
||||||
|
|
@ -286,7 +305,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
||||||
LeaveWorld: false,
|
LeaveWorld: false,
|
||||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||||
StopInterpolating: false,
|
StopInterpolating: false,
|
||||||
ConstrainAfterRouting: false,
|
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||||
PreserveHeading: true,
|
PreserveHeading: true,
|
||||||
ZeroVelocity: false,
|
ZeroVelocity: false,
|
||||||
SendPositionImmediately: true,
|
SendPositionImmediately: true,
|
||||||
|
|
@ -309,7 +328,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
||||||
LeaveWorld: false,
|
LeaveWorld: false,
|
||||||
TeleportHookPhase: RuntimeTeleportHookPhase.AfterPositionOperation,
|
TeleportHookPhase: RuntimeTeleportHookPhase.AfterPositionOperation,
|
||||||
StopInterpolating: false,
|
StopInterpolating: false,
|
||||||
ConstrainAfterRouting: true,
|
ConstrainPhase: RuntimePositionConstrainPhase.AfterPositionOperation,
|
||||||
PreserveHeading: false,
|
PreserveHeading: false,
|
||||||
ZeroVelocity: true,
|
ZeroVelocity: true,
|
||||||
SendPositionImmediately: false,
|
SendPositionImmediately: false,
|
||||||
|
|
@ -331,7 +350,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
||||||
LeaveWorld: false,
|
LeaveWorld: false,
|
||||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||||
StopInterpolating: false,
|
StopInterpolating: false,
|
||||||
ConstrainAfterRouting: true,
|
ConstrainPhase: RuntimePositionConstrainPhase.BeforePositionOperation,
|
||||||
PreserveHeading: false,
|
PreserveHeading: false,
|
||||||
ZeroVelocity: false,
|
ZeroVelocity: false,
|
||||||
SendPositionImmediately: false,
|
SendPositionImmediately: false,
|
||||||
|
|
@ -357,7 +376,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
||||||
LeaveWorld: false,
|
LeaveWorld: false,
|
||||||
TeleportHookPhase: RuntimeTeleportHookPhase.BeforePositionOperation,
|
TeleportHookPhase: RuntimeTeleportHookPhase.BeforePositionOperation,
|
||||||
StopInterpolating: false,
|
StopInterpolating: false,
|
||||||
ConstrainAfterRouting: true,
|
ConstrainPhase: RuntimePositionConstrainPhase.AfterPositionOperation,
|
||||||
PreserveHeading: false,
|
PreserveHeading: false,
|
||||||
ZeroVelocity: false,
|
ZeroVelocity: false,
|
||||||
SendPositionImmediately: false,
|
SendPositionImmediately: false,
|
||||||
|
|
@ -383,7 +402,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
||||||
LeaveWorld: false,
|
LeaveWorld: false,
|
||||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||||
StopInterpolating: false,
|
StopInterpolating: false,
|
||||||
ConstrainAfterRouting: false,
|
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||||
PreserveHeading: false,
|
PreserveHeading: false,
|
||||||
ZeroVelocity: false,
|
ZeroVelocity: false,
|
||||||
SendPositionImmediately: false,
|
SendPositionImmediately: false,
|
||||||
|
|
@ -413,7 +432,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
||||||
LeaveWorld: false,
|
LeaveWorld: false,
|
||||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||||
StopInterpolating: !nearby,
|
StopInterpolating: !nearby,
|
||||||
ConstrainAfterRouting: true,
|
ConstrainPhase: RuntimePositionConstrainPhase.AfterPositionOperation,
|
||||||
PreserveHeading: false,
|
PreserveHeading: false,
|
||||||
ZeroVelocity: false,
|
ZeroVelocity: false,
|
||||||
SendPositionImmediately: false,
|
SendPositionImmediately: false,
|
||||||
|
|
@ -445,7 +464,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
||||||
LeaveWorld: true,
|
LeaveWorld: true,
|
||||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||||
StopInterpolating: false,
|
StopInterpolating: false,
|
||||||
ConstrainAfterRouting: false,
|
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||||
PreserveHeading: false,
|
PreserveHeading: false,
|
||||||
ZeroVelocity: false,
|
ZeroVelocity: false,
|
||||||
SendPositionImmediately: false,
|
SendPositionImmediately: false,
|
||||||
|
|
@ -532,7 +551,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
||||||
LeaveWorld: false,
|
LeaveWorld: false,
|
||||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||||
StopInterpolating: false,
|
StopInterpolating: false,
|
||||||
ConstrainAfterRouting: false,
|
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||||
PreserveHeading: false,
|
PreserveHeading: false,
|
||||||
ZeroVelocity: false,
|
ZeroVelocity: false,
|
||||||
SendPositionImmediately: false,
|
SendPositionImmediately: false,
|
||||||
|
|
@ -552,7 +571,7 @@ internal static class RuntimeAuthoritativePositionRouteClassifier
|
||||||
LeaveWorld: false,
|
LeaveWorld: false,
|
||||||
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
||||||
StopInterpolating: false,
|
StopInterpolating: false,
|
||||||
ConstrainAfterRouting: false,
|
ConstrainPhase: RuntimePositionConstrainPhase.None,
|
||||||
PreserveHeading: false,
|
PreserveHeading: false,
|
||||||
ZeroVelocity: false,
|
ZeroVelocity: false,
|
||||||
SendPositionImmediately: false,
|
SendPositionImmediately: false,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -140,6 +140,7 @@ public sealed class RuntimeAuthoritativePositionRouteClassifierTests
|
||||||
playerDistance: distance);
|
playerDistance: distance);
|
||||||
|
|
||||||
Assert.Equal(expected, route.Disposition);
|
Assert.Equal(expected, route.Disposition);
|
||||||
|
Assert.False(route.ConstrainBeforeRouting);
|
||||||
Assert.True(route.ConstrainAfterRouting);
|
Assert.True(route.ConstrainAfterRouting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -326,7 +327,8 @@ public sealed class RuntimeAuthoritativePositionRouteClassifierTests
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
Assert.Equal(expected, route.Disposition);
|
Assert.Equal(expected, route.Disposition);
|
||||||
Assert.True(route.ConstrainAfterRouting);
|
Assert.True(route.ConstrainBeforeRouting);
|
||||||
|
Assert.False(route.ConstrainAfterRouting);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue