632 lines
22 KiB
C#
632 lines
22 KiB
C#
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
|
|
namespace AcDream.Runtime.Entities;
|
|
|
|
/// <summary>
|
|
/// Update-thread state machine for parent relations that may arrive before
|
|
/// either CreateObject. Unaccepted wire events remain in arrival order;
|
|
/// committed rollback history contains only relations that passed both the
|
|
/// canonical retail timestamp gate and retail <c>PartArray::add_child</c>
|
|
/// validation.
|
|
/// </summary>
|
|
public sealed class ParentAttachmentState
|
|
{
|
|
private readonly Dictionary<uint, Queue<ParentAttachmentRelation>> _unresolvedByChild = new();
|
|
private readonly Dictionary<uint, ParentAttachmentRelation> _stagedByChild = new();
|
|
private readonly Dictionary<uint, ParentAttachmentRelation> _recoveryByChild = new();
|
|
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = new();
|
|
private readonly Dictionary<ParentIncarnation, List<uint>> _committedChildrenByParent = new();
|
|
private readonly Dictionary<uint, Queue<DeferredParentCreate>>
|
|
_deferredCreatesByParent = [];
|
|
private ulong _nextDeferredCreateAdmissionId;
|
|
|
|
public int UnresolvedRelationCount =>
|
|
_unresolvedByChild.Values.Sum(queue => queue.Count);
|
|
public int StagedRelationCount => _stagedByChild.Count;
|
|
public int RecoveryRelationCount => _recoveryByChild.Count;
|
|
public int CommittedRelationCount => _lastAcceptedByChild.Count;
|
|
internal int DeferredCreateCount =>
|
|
_deferredCreatesByParent.Values.Sum(queue => queue.Count);
|
|
|
|
/// <summary>
|
|
/// Retains the complete unaccepted CreateObject packet when its nonzero
|
|
/// parent is not addressable. Retail queues the raw blob before object
|
|
/// lookup and before any child timestamp is consumed; storing a decoded
|
|
/// relation after AcceptCreate would partially admit the child.
|
|
/// </summary>
|
|
internal void EnqueueDeferredCreate(
|
|
WorldSession.EntitySpawn spawn,
|
|
bool isLocalPlayer)
|
|
{
|
|
uint parentGuid = spawn.ParentGuid
|
|
?? spawn.Physics?.Parent?.Guid
|
|
?? 0u;
|
|
if (spawn.Guid == 0u || parentGuid == 0u)
|
|
{
|
|
throw new ArgumentException(
|
|
"A deferred parent CreateObject requires nonzero child and parent GUIDs.",
|
|
nameof(spawn));
|
|
}
|
|
if (_nextDeferredCreateAdmissionId == ulong.MaxValue)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The deferred parent CreateObject admission sequence is exhausted.");
|
|
}
|
|
if (!_deferredCreatesByParent.TryGetValue(
|
|
parentGuid,
|
|
out Queue<DeferredParentCreate>? queue))
|
|
{
|
|
queue = new Queue<DeferredParentCreate>();
|
|
_deferredCreatesByParent.Add(parentGuid, queue);
|
|
}
|
|
ulong admissionId = _nextDeferredCreateAdmissionId + 1UL;
|
|
queue.Enqueue(new DeferredParentCreate(
|
|
admissionId,
|
|
RuntimeInitialCreateAdmissionFreezer.Freeze(spawn),
|
|
isLocalPlayer));
|
|
_nextDeferredCreateAdmissionId = admissionId;
|
|
}
|
|
|
|
internal bool TryPeekDeferredCreate(
|
|
uint parentGuid,
|
|
out DeferredParentCreate deferred)
|
|
{
|
|
if (!_deferredCreatesByParent.TryGetValue(
|
|
parentGuid,
|
|
out Queue<DeferredParentCreate>? queue)
|
|
|| !queue.TryPeek(out deferred))
|
|
{
|
|
deferred = default;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
internal bool ConsumeDeferredCreate(
|
|
uint parentGuid,
|
|
in DeferredParentCreate expected)
|
|
{
|
|
if (!_deferredCreatesByParent.TryGetValue(
|
|
parentGuid,
|
|
out Queue<DeferredParentCreate>? queue)
|
|
|| !queue.TryPeek(out DeferredParentCreate current)
|
|
|| current != expected)
|
|
{
|
|
return false;
|
|
}
|
|
_ = queue.Dequeue();
|
|
if (queue.Count == 0)
|
|
_deferredCreatesByParent.Remove(parentGuid);
|
|
return true;
|
|
}
|
|
|
|
internal bool ContainsDeferredCreate(
|
|
uint childGuid,
|
|
ushort instanceSequence)
|
|
{
|
|
foreach (Queue<DeferredParentCreate> queue
|
|
in _deferredCreatesByParent.Values)
|
|
{
|
|
if (queue.Any(candidate =>
|
|
candidate.Spawn.Guid == childGuid
|
|
&& candidate.Spawn.InstanceSequence == instanceSequence))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancels only the raw, still-unaccepted child generation addressed by a
|
|
/// terminal packet. Instance zero is a normal retail timestamp and is not
|
|
/// treated as an empty sentinel.
|
|
/// </summary>
|
|
internal void CancelDeferredChildGeneration(
|
|
uint childGuid,
|
|
ushort terminalInstanceSequence) => FilterDeferredCreates(
|
|
candidate => candidate.Spawn.Guid != childGuid
|
|
|| PhysicsTimestampGate.IsNewer(
|
|
terminalInstanceSequence,
|
|
candidate.Spawn.InstanceSequence));
|
|
|
|
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
|
|
{
|
|
_stagedByChild[relation.ChildGuid] = relation;
|
|
}
|
|
|
|
public void Enqueue(ParentEvent.Parsed update)
|
|
{
|
|
if (!_unresolvedByChild.TryGetValue(update.ChildGuid, out Queue<ParentAttachmentRelation>? queue))
|
|
{
|
|
queue = new Queue<ParentAttachmentRelation>();
|
|
_unresolvedByChild.Add(update.ChildGuid, queue);
|
|
}
|
|
|
|
queue.Enqueue(new ParentAttachmentRelation(
|
|
update.ParentGuid,
|
|
update.ChildGuid,
|
|
update.ParentLocation,
|
|
update.PlacementId,
|
|
update.ParentInstanceSequence,
|
|
update.ChildPositionSequence));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves every now-addressable relation for one child in wire order.
|
|
/// A future parent generation remains queued; an older generation or a
|
|
/// rejected child POSITION_TS is discarded. Every accepted relation
|
|
/// supersedes the preceding render projection.
|
|
/// </summary>
|
|
public void Resolve(
|
|
uint childGuid,
|
|
Func<uint, bool> isObjectKnown,
|
|
Func<uint, ushort?> resolveInstance,
|
|
Func<ParentEvent.Parsed, bool> accept)
|
|
{
|
|
if (_stagedByChild.ContainsKey(childGuid))
|
|
return;
|
|
if (!_unresolvedByChild.TryGetValue(childGuid, out Queue<ParentAttachmentRelation>? queue))
|
|
return;
|
|
int candidateCount = queue.Count;
|
|
for (int i = 0; i < candidateCount; i++)
|
|
{
|
|
ParentAttachmentRelation relation = queue.Dequeue();
|
|
ushort? parentInstance = resolveInstance(relation.ParentGuid);
|
|
if (parentInstance is null)
|
|
{
|
|
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Parent });
|
|
continue;
|
|
}
|
|
|
|
if (parentInstance.Value != relation.ParentInstanceSequence)
|
|
{
|
|
// Current parent newer than the packet: discard the stale
|
|
// relation. Packet newer than current: retain it until that
|
|
// parent generation is constructed.
|
|
if (PhysicsTimestampGate.IsNewer(
|
|
relation.ParentInstanceSequence,
|
|
parentInstance.Value))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Parent });
|
|
continue;
|
|
}
|
|
|
|
if (!isObjectKnown(childGuid))
|
|
{
|
|
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Child });
|
|
continue;
|
|
}
|
|
|
|
var update = new ParentEvent.Parsed(
|
|
relation.ParentGuid,
|
|
relation.ChildGuid,
|
|
relation.ParentLocation,
|
|
relation.PlacementId,
|
|
relation.ParentInstanceSequence,
|
|
relation.ChildPositionSequence);
|
|
if (!accept(update))
|
|
continue;
|
|
|
|
_stagedByChild[childGuid] = relation with
|
|
{
|
|
WaitOwner = ParentAttachmentWaitOwner.Unknown,
|
|
};
|
|
break;
|
|
}
|
|
|
|
if (queue.Count == 0)
|
|
_unresolvedByChild.Remove(childGuid);
|
|
}
|
|
|
|
public bool TryGetProjection(uint childGuid, out ParentAttachmentRelation relation) =>
|
|
_stagedByChild.TryGetValue(childGuid, out relation)
|
|
|| _recoveryByChild.TryGetValue(childGuid, out relation);
|
|
|
|
public bool TryGetStagedProjection(
|
|
uint childGuid,
|
|
out ParentAttachmentRelation relation) =>
|
|
_stagedByChild.TryGetValue(childGuid, out relation);
|
|
|
|
public bool TryGetRecoveryProjection(
|
|
uint childGuid,
|
|
out ParentAttachmentRelation relation) =>
|
|
_recoveryByChild.TryGetValue(childGuid, out relation);
|
|
|
|
public void CopyPendingProjectionChildrenTo(List<uint> destination)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(destination);
|
|
destination.Clear();
|
|
foreach (uint childGuid in _stagedByChild.Keys)
|
|
destination.Add(childGuid);
|
|
foreach (uint childGuid in _recoveryByChild.Keys)
|
|
{
|
|
if (!_stagedByChild.ContainsKey(childGuid))
|
|
destination.Add(childGuid);
|
|
}
|
|
}
|
|
|
|
public bool IsCommitted(ParentAttachmentRelation relation) =>
|
|
_lastAcceptedByChild.TryGetValue(
|
|
relation.ChildGuid,
|
|
out ParentAttachmentRelation committed)
|
|
&& committed == relation;
|
|
|
|
public bool HasCommittedParent(uint childGuid) =>
|
|
_lastAcceptedByChild.ContainsKey(childGuid);
|
|
|
|
public bool IsPending(
|
|
ParentAttachmentRelation relation,
|
|
ParentProjectionCandidateKind kind) =>
|
|
(kind is ParentProjectionCandidateKind.Staged
|
|
? _stagedByChild
|
|
: _recoveryByChild).TryGetValue(
|
|
relation.ChildGuid,
|
|
out ParentAttachmentRelation pending)
|
|
&& pending == relation;
|
|
|
|
public void MarkProjected(
|
|
ParentAttachmentRelation relation,
|
|
ParentProjectionCandidateKind kind)
|
|
{
|
|
Dictionary<uint, ParentAttachmentRelation> source =
|
|
kind is ParentProjectionCandidateKind.Staged
|
|
? _stagedByChild
|
|
: _recoveryByChild;
|
|
if (source.TryGetValue(
|
|
relation.ChildGuid,
|
|
out ParentAttachmentRelation pending)
|
|
&& pending == relation)
|
|
{
|
|
source.Remove(relation.ChildGuid);
|
|
}
|
|
}
|
|
|
|
public bool CommitProjection(ParentAttachmentRelation relation)
|
|
{
|
|
if (!_stagedByChild.TryGetValue(
|
|
relation.ChildGuid,
|
|
out ParentAttachmentRelation staged)
|
|
|| staged != relation)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
RemoveCommittedChild(relation.ChildGuid);
|
|
_lastAcceptedByChild[relation.ChildGuid] = relation;
|
|
var parent = new ParentIncarnation(
|
|
relation.ParentGuid,
|
|
relation.ParentInstanceSequence);
|
|
if (!_committedChildrenByParent.TryGetValue(
|
|
parent,
|
|
out List<uint>? children))
|
|
{
|
|
children = [];
|
|
_committedChildrenByParent.Add(parent, children);
|
|
}
|
|
children.Add(relation.ChildGuid);
|
|
_stagedByChild.Remove(relation.ChildGuid);
|
|
_recoveryByChild[relation.ChildGuid] = relation;
|
|
return true;
|
|
}
|
|
|
|
public void RejectProjection(ParentAttachmentRelation relation)
|
|
{
|
|
if (_stagedByChild.TryGetValue(
|
|
relation.ChildGuid,
|
|
out ParentAttachmentRelation staged)
|
|
&& staged == relation)
|
|
{
|
|
_stagedByChild.Remove(relation.ChildGuid);
|
|
}
|
|
}
|
|
|
|
public bool RestoreLastAccepted(uint childGuid)
|
|
{
|
|
if (!_lastAcceptedByChild.TryGetValue(childGuid, out ParentAttachmentRelation relation))
|
|
return false;
|
|
_recoveryByChild[childGuid] = relation;
|
|
return true;
|
|
}
|
|
|
|
public IReadOnlyList<uint> ChildrenWaitingForParent(uint parentGuid)
|
|
{
|
|
var result = new HashSet<uint>();
|
|
foreach ((uint childGuid, ParentAttachmentRelation relation) in _stagedByChild)
|
|
{
|
|
if (relation.ParentGuid == parentGuid)
|
|
result.Add(childGuid);
|
|
}
|
|
|
|
foreach ((uint childGuid, ParentAttachmentRelation relation) in _recoveryByChild)
|
|
{
|
|
if (relation.ParentGuid == parentGuid)
|
|
result.Add(childGuid);
|
|
}
|
|
|
|
foreach ((uint childGuid, Queue<ParentAttachmentRelation> queue) in _unresolvedByChild)
|
|
{
|
|
if (queue.Any(relation => relation.ParentGuid == parentGuid))
|
|
result.Add(childGuid);
|
|
}
|
|
|
|
return result.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns only the exact direct children currently committed to one
|
|
/// parent incarnation. Lost-cell destruction follows retail's live
|
|
/// CHILDLIST and must not capture staged, unresolved, or future-generation
|
|
/// relations that merely reuse the same parent GUID.
|
|
/// </summary>
|
|
public IReadOnlyList<uint> ChildrenAttachedToParent(
|
|
uint parentGuid,
|
|
ushort parentInstanceSequence)
|
|
{
|
|
var parent = new ParentIncarnation(parentGuid, parentInstanceSequence);
|
|
return _committedChildrenByParent.TryGetValue(
|
|
parent,
|
|
out List<uint>? children)
|
|
? children
|
|
: Array.Empty<uint>();
|
|
}
|
|
|
|
public void RemoveObject(uint guid)
|
|
{
|
|
RemoveDeferredChildCreates(guid);
|
|
_stagedByChild.Remove(guid);
|
|
_recoveryByChild.Remove(guid);
|
|
RemoveCommittedChild(guid);
|
|
_unresolvedByChild.Remove(guid);
|
|
|
|
RemoveParentReferences(_stagedByChild, guid);
|
|
RemoveParentReferences(_recoveryByChild, guid);
|
|
RemoveCommittedParentReferences(guid);
|
|
|
|
uint[] children = _unresolvedByChild.Keys.ToArray();
|
|
for (int i = 0; i < children.Length; i++)
|
|
{
|
|
uint childGuid = children[i];
|
|
Queue<ParentAttachmentRelation> retained = new(
|
|
_unresolvedByChild[childGuid].Where(relation => relation.ParentGuid != guid));
|
|
if (retained.Count == 0)
|
|
_unresolvedByChild.Remove(childGuid);
|
|
else
|
|
_unresolvedByChild[childGuid] = retained;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends one logical incarnation without discarding unresolved wire events
|
|
/// that may explicitly address the replacement/future parent generation.
|
|
/// </summary>
|
|
public void EndGeneration(uint guid, ushort replacementGeneration)
|
|
{
|
|
FilterDeferredCreates(candidate =>
|
|
candidate.Spawn.Guid != guid
|
|
|| candidate.Spawn.InstanceSequence == replacementGeneration
|
|
|| PhysicsTimestampGate.IsNewer(
|
|
replacementGeneration,
|
|
candidate.Spawn.InstanceSequence));
|
|
FilterChildCandidates(
|
|
guid,
|
|
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
|
_stagedByChild.Remove(guid);
|
|
_recoveryByChild.Remove(guid);
|
|
RemoveCommittedChild(guid);
|
|
RemoveParentReferences(_stagedByChild, guid);
|
|
RemoveParentReferences(_recoveryByChild, guid);
|
|
RemoveCommittedParentReferences(guid);
|
|
FilterParentCandidates(
|
|
guid,
|
|
relation => relation.ParentInstanceSequence == replacementGeneration
|
|
|| PhysicsTimestampGate.IsNewer(
|
|
replacementGeneration,
|
|
relation.ParentInstanceSequence));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes one exact incarnation. Child-addressed candidates die with the
|
|
/// child; candidates addressed to a strictly newer parent incarnation
|
|
/// survive for retail's post-Create blob replay.
|
|
/// </summary>
|
|
public void DeleteGeneration(uint guid, ushort deletedGeneration)
|
|
{
|
|
CancelDeferredChildGeneration(guid, deletedGeneration);
|
|
FilterChildCandidates(
|
|
guid,
|
|
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
|
_stagedByChild.Remove(guid);
|
|
_recoveryByChild.Remove(guid);
|
|
RemoveCommittedChild(guid);
|
|
RemoveParentReferences(_stagedByChild, guid);
|
|
RemoveParentReferences(_recoveryByChild, guid);
|
|
RemoveCommittedParentReferences(guid);
|
|
FilterParentCandidates(
|
|
guid,
|
|
relation => PhysicsTimestampGate.IsNewer(
|
|
deletedGeneration,
|
|
relation.ParentInstanceSequence));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears an accepted attachment projection after Pickup or a world
|
|
/// Position without destroying fresher unresolved ParentEvents.
|
|
/// </summary>
|
|
public void EndChildProjection(uint childGuid)
|
|
{
|
|
_stagedByChild.Remove(childGuid);
|
|
_recoveryByChild.Remove(childGuid);
|
|
RemoveCommittedChild(childGuid);
|
|
}
|
|
|
|
public void RemoveChild(uint childGuid)
|
|
{
|
|
RemoveDeferredChildCreates(childGuid);
|
|
_stagedByChild.Remove(childGuid);
|
|
_recoveryByChild.Remove(childGuid);
|
|
RemoveCommittedChild(childGuid);
|
|
_unresolvedByChild.Remove(childGuid);
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
_deferredCreatesByParent.Clear();
|
|
_unresolvedByChild.Clear();
|
|
_stagedByChild.Clear();
|
|
_recoveryByChild.Clear();
|
|
_lastAcceptedByChild.Clear();
|
|
foreach (List<uint> children in _committedChildrenByParent.Values)
|
|
children.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)
|
|
{
|
|
if (!_lastAcceptedByChild.Remove(
|
|
childGuid,
|
|
out ParentAttachmentRelation relation))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var parent = new ParentIncarnation(
|
|
relation.ParentGuid,
|
|
relation.ParentInstanceSequence);
|
|
if (!_committedChildrenByParent.TryGetValue(
|
|
parent,
|
|
out List<uint>? children))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int childIndex = children.IndexOf(childGuid);
|
|
if (childIndex >= 0)
|
|
{
|
|
int lastIndex = children.Count - 1;
|
|
children[childIndex] = children[lastIndex];
|
|
children.RemoveAt(lastIndex);
|
|
}
|
|
|
|
if (children.Count == 0)
|
|
_committedChildrenByParent.Remove(parent);
|
|
}
|
|
|
|
private void RemoveCommittedParentReferences(uint parentGuid)
|
|
{
|
|
uint[] children = _lastAcceptedByChild
|
|
.Where(pair => pair.Value.ParentGuid == parentGuid)
|
|
.Select(pair => pair.Key)
|
|
.ToArray();
|
|
for (int i = 0; i < children.Length; i++)
|
|
RemoveCommittedChild(children[i]);
|
|
}
|
|
|
|
private static void RemoveParentReferences(
|
|
Dictionary<uint, ParentAttachmentRelation> relations,
|
|
uint parentGuid)
|
|
{
|
|
uint[] children = relations
|
|
.Where(pair => pair.Value.ParentGuid == parentGuid)
|
|
.Select(pair => pair.Key)
|
|
.ToArray();
|
|
for (int i = 0; i < children.Length; i++)
|
|
relations.Remove(children[i]);
|
|
}
|
|
|
|
private void FilterParentCandidates(
|
|
uint parentGuid,
|
|
Func<ParentAttachmentRelation, bool> retain)
|
|
{
|
|
uint[] children = _unresolvedByChild.Keys.ToArray();
|
|
for (int i = 0; i < children.Length; i++)
|
|
{
|
|
uint childGuid = children[i];
|
|
Queue<ParentAttachmentRelation> queue = _unresolvedByChild[childGuid];
|
|
var retained = new Queue<ParentAttachmentRelation>(
|
|
queue.Where(relation => relation.ParentGuid != parentGuid || retain(relation)));
|
|
if (retained.Count == 0)
|
|
_unresolvedByChild.Remove(childGuid);
|
|
else
|
|
_unresolvedByChild[childGuid] = retained;
|
|
}
|
|
}
|
|
|
|
private void FilterChildCandidates(
|
|
uint childGuid,
|
|
Func<ParentAttachmentRelation, bool> retain)
|
|
{
|
|
if (!_unresolvedByChild.TryGetValue(
|
|
childGuid,
|
|
out Queue<ParentAttachmentRelation>? queue))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var retained = new Queue<ParentAttachmentRelation>(queue.Where(retain));
|
|
if (retained.Count == 0)
|
|
_unresolvedByChild.Remove(childGuid);
|
|
else
|
|
_unresolvedByChild[childGuid] = retained;
|
|
}
|
|
|
|
private readonly record struct ParentIncarnation(
|
|
uint ServerGuid,
|
|
ushort InstanceSequence);
|
|
}
|
|
|
|
internal readonly record struct DeferredParentCreate(
|
|
ulong AdmissionId,
|
|
WorldSession.EntitySpawn Spawn,
|
|
bool IsLocalPlayer)
|
|
{
|
|
internal bool IsValid => AdmissionId != 0UL
|
|
&& Spawn.Guid != 0u;
|
|
}
|
|
|
|
public readonly record struct ParentAttachmentRelation(
|
|
uint ParentGuid,
|
|
uint ChildGuid,
|
|
uint ParentLocation,
|
|
uint PlacementId,
|
|
ushort ParentInstanceSequence,
|
|
ushort ChildPositionSequence,
|
|
ParentAttachmentWaitOwner WaitOwner = ParentAttachmentWaitOwner.Unknown);
|
|
|
|
public enum ParentAttachmentWaitOwner
|
|
{
|
|
Unknown,
|
|
Parent,
|
|
Child,
|
|
}
|
|
|
|
public enum ParentProjectionCandidateKind
|
|
{
|
|
Staged,
|
|
Recovery,
|
|
}
|