Move ObjDesc, Parent, Pickup, child-ready, and retained projection recovery into LiveEntityHydrationController while preserving retail timestamp and leave-world semantics. Pin ready publication to exact projection authority and restore attached descendant chains synchronously.
1526 lines
57 KiB
C#
1526 lines
57 KiB
C#
using System.Numerics;
|
|
using AcDream.App.World;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Meshing;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter;
|
|
using AcDream.Content;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Enums;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// Render-side owner of retail parented physics objects (held weapons,
|
|
/// shields, and visible ammunition). Network/state parsing remains in Core;
|
|
/// this controller projects an accepted parent relation into a dynamic child
|
|
/// <see cref="WorldEntity"/> and recomposes it after the parent's animation
|
|
/// advances each frame.
|
|
/// </summary>
|
|
public sealed class EquippedChildRenderController : IDisposable
|
|
{
|
|
private readonly IDatReaderWriter _dats;
|
|
private readonly object _datLock;
|
|
private readonly ClientObjectTable _objects;
|
|
private readonly LiveEntityRuntime _liveEntities;
|
|
private readonly Func<ParentEvent.Parsed, bool> _acceptParent;
|
|
private readonly Func<LiveEntityRecord, ulong, ulong, ExactProjectionWithdrawalOutcome>
|
|
_withdrawProjection;
|
|
private readonly EntityEffectPoseRegistry _poses;
|
|
|
|
private ParentAttachmentState Relations => _liveEntities.ParentAttachments;
|
|
|
|
/// <summary>Raised after the attached projection is fully registered.</summary>
|
|
internal event Action<LiveEntityReadyCandidate>? EntityReady;
|
|
/// <summary>Raised after an attached projection has its composed pose.</summary>
|
|
public event Action<uint>? ProjectionPoseReady;
|
|
/// <summary>Raised when an attached projection leaves its cell presentation.</summary>
|
|
public event Action<uint>? ProjectionRemoved;
|
|
private readonly Dictionary<uint, AttachedChild> _attachedByChild = new();
|
|
private readonly Dictionary<uint, PendingUnparentTransition> _pendingUnparentByChild = new();
|
|
private readonly Dictionary<uint, PendingOrdinaryRemoval> _pendingOrdinaryRemovalByRoot = new();
|
|
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingDetachedRemovalByChild = new();
|
|
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingReparentRemovalByChild = new();
|
|
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingPoseLossRemovalByChild = new();
|
|
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingOrphanRemovalByChild = new();
|
|
private readonly List<uint> _pendingProjectionChildrenScratch = new();
|
|
private readonly AttachmentUpdateOrder<AttachedChild> _updateOrder = new();
|
|
private readonly Func<AttachedChild, uint?> _parentOfAttached;
|
|
private readonly Func<uint, bool> _tickAttached;
|
|
|
|
public IEnumerable<uint> AttachedEntityIds
|
|
{
|
|
get
|
|
{
|
|
foreach (AttachedChild child in _attachedByChild.Values)
|
|
yield return child.Entity.Id;
|
|
}
|
|
}
|
|
|
|
public EquippedChildRenderController(
|
|
IDatReaderWriter dats,
|
|
object datLock,
|
|
ClientObjectTable objects,
|
|
LiveEntityRuntime liveEntities,
|
|
EntityEffectPoseRegistry poses,
|
|
Func<ParentEvent.Parsed, bool> acceptParent,
|
|
Func<LiveEntityRecord, ulong, ulong, ExactProjectionWithdrawalOutcome>
|
|
withdrawProjection)
|
|
{
|
|
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
|
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
|
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
|
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
|
_poses = poses ?? throw new ArgumentNullException(nameof(poses));
|
|
_acceptParent = acceptParent ?? throw new ArgumentNullException(nameof(acceptParent));
|
|
_withdrawProjection = withdrawProjection
|
|
?? throw new ArgumentNullException(nameof(withdrawProjection));
|
|
_parentOfAttached = static child => child.ParentGuid;
|
|
_tickAttached = TickChild;
|
|
|
|
_objects.ObjectMoved += OnObjectMoved;
|
|
_objects.MoveRolledBack += OnMoveRolledBack;
|
|
_objects.ObjectRemovalClassified += OnObjectRemovalClassified;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seed/refresh an object from CreateObject. Equipped child CreateObjects
|
|
/// carry Placement + Parent directly; this is retail's login/first-observe
|
|
/// bootstrap and does not wait for a separate ParentEvent.
|
|
/// </summary>
|
|
public void OnSpawn(WorldSession.EntitySpawn spawn)
|
|
{
|
|
lock (_datLock)
|
|
{
|
|
if (spawn.ParentGuid is { } parentGuid and not 0
|
|
&& spawn.ParentLocation is { } parentLocation)
|
|
{
|
|
// PhysicsDesc::PhysicsDesc / UnPack (0x0051D4D0 /
|
|
// 0x0051DDD0) default an absent AnimationFrame to zero.
|
|
// Parent remains a complete relation in that case.
|
|
uint placementId = spawn.PlacementId ?? 0u;
|
|
Relations.AcceptCreateObjectRelation(new ParentAttachmentRelation(
|
|
parentGuid,
|
|
spawn.Guid,
|
|
parentLocation,
|
|
placementId,
|
|
ParentInstanceSequence: 0,
|
|
spawn.PositionSequence));
|
|
}
|
|
|
|
ResolveAndTryRealize(spawn.Guid);
|
|
|
|
// ParentEvent can precede the parent's CreateObject. Revisit the
|
|
// complete descendant chain rooted at the object that arrived.
|
|
RetryWaitingDescendants(spawn.Guid);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail <c>SmartBox::UpdateVisualDesc @ 0x00451F40</c> replaces an
|
|
/// attached child's visual description without requiring a world
|
|
/// Position. Re-realize the last accepted relation from the newest
|
|
/// canonical snapshot while retaining the logical entity.
|
|
/// </summary>
|
|
internal bool TryApplyAttachedAppearance(
|
|
LiveEntityRecord expectedRecord,
|
|
ulong expectedObjDescAuthorityVersion)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
|
lock (_datLock)
|
|
{
|
|
if (!_liveEntities.IsCurrentObjDescAuthority(
|
|
expectedRecord,
|
|
expectedObjDescAuthorityVersion)
|
|
|| expectedRecord.ProjectionKind is not
|
|
LiveEntityProjectionKind.Attached
|
|
|| !expectedRecord.IsSpatiallyProjected
|
|
|| expectedRecord.WorldEntity is not { } expectedEntity
|
|
|| !Relations.RestoreLastAccepted(expectedRecord.ServerGuid))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool projected = ResolveAndTryRealize(expectedRecord.ServerGuid);
|
|
if (projected)
|
|
{
|
|
// WithdrawPriorProjection removes the complete attached
|
|
// subtree and retains each descendant relation for recovery.
|
|
// The ObjDesc transaction is not published until that same
|
|
// subtree is synchronously whole again.
|
|
RetryWaitingDescendants(expectedRecord.ServerGuid);
|
|
}
|
|
return projected
|
|
&& _liveEntities.IsCurrentObjDescAuthority(
|
|
expectedRecord,
|
|
expectedObjDescAuthorityVersion)
|
|
&& expectedRecord.ProjectionKind is
|
|
LiveEntityProjectionKind.Attached
|
|
&& expectedRecord.IsSpatiallyProjected
|
|
&& ReferenceEquals(expectedRecord.WorldEntity, expectedEntity);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes attachment projection after a root world entity has been
|
|
/// registered. Relation acceptance intentionally happens before root
|
|
/// projection selection; this notification only satisfies render-pose
|
|
/// dependencies for children waiting on that root.
|
|
/// </summary>
|
|
public void OnWorldEntityRegistered(uint guid)
|
|
{
|
|
lock (_datLock)
|
|
{
|
|
RetryWaitingDescendants(guid);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retries pose-withdrawn descendants only after their parent's new root
|
|
/// and indexed parts have been published.
|
|
/// </summary>
|
|
public void OnPosePublished(uint guid)
|
|
{
|
|
lock (_datLock)
|
|
RetryWaitingDescendants(guid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply/queue a live ParentEvent after the live-entity owner has exact-
|
|
/// gated the parent's INSTANCE_TS and advanced the child's shared
|
|
/// POSITION_TS. This controller owns only the render relationship.
|
|
/// </summary>
|
|
public void OnParentEvent(ParentEvent.Parsed update)
|
|
{
|
|
lock (_datLock)
|
|
{
|
|
Relations.Enqueue(update);
|
|
if (ResolveAndTryRealize(update.ChildGuid))
|
|
RetryWaitingDescendants(update.ChildGuid);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes projections owned by one retired incarnation. The runtime has
|
|
/// already removed that record from its active GUID table, so the root's
|
|
/// attached-map entry is committed by exact record identity without a
|
|
/// GUID lookup. Still-live descendants use the normal exact withdrawal
|
|
/// path and remain eligible to replay their retained parent relation.
|
|
/// </summary>
|
|
public void OnLogicalTeardown(LiveEntityRecord record)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
lock (_datLock)
|
|
TearDownRecordProjections(record);
|
|
}
|
|
|
|
private void OnObjectRemovalClassified(ClientObjectRemoval removal)
|
|
{
|
|
if (removal.Reason is not ClientObjectRemovalReason.Ordinary)
|
|
return;
|
|
|
|
uint guid = removal.Object.ObjectId;
|
|
TearDownCurrentObjectProjections(guid);
|
|
// InventoryRemoveObject removes the item from the UI view, not the
|
|
// live physics generation. Preserve fresher pending ParentEvents so a
|
|
// same-generation world/parent update can still replay them. Logical
|
|
// delete/replacement are driven directly by the accepted inbound
|
|
// lifecycle and never depend on this table.
|
|
}
|
|
|
|
/// <summary>
|
|
/// A fresh Position or Pickup unparented this child. Remove the accepted
|
|
/// attachment/rollback projection while retaining fresher unresolved
|
|
/// ParentEvents; unlike logical deletion, do not disturb child-addressed
|
|
/// pending state or children that may themselves reference it.
|
|
/// </summary>
|
|
public ChildUnparentDisposition OnChildBecameUnparented(
|
|
uint childGuid,
|
|
Action? continuation = null)
|
|
{
|
|
if (!_liveEntities.TryGetRecord(childGuid, out LiveEntityRecord record))
|
|
{
|
|
_pendingUnparentByChild.Remove(childGuid);
|
|
Relations.EndChildProjection(childGuid);
|
|
return ChildUnparentDisposition.NotAttached;
|
|
}
|
|
|
|
var pending = new PendingUnparentTransition(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
CaptureProjectionSubtreeParentFirst(
|
|
record,
|
|
restoreRootRelation: false,
|
|
restoreDescendantRelations: true),
|
|
continuation);
|
|
return AdvanceUnparentTransition(childGuid, pending);
|
|
}
|
|
|
|
/// <summary>Recompose every child after the parent's animation tick.</summary>
|
|
public void Tick()
|
|
{
|
|
RetryPendingProjectionTransitions();
|
|
IReadOnlyList<uint> failed = _updateOrder.ForEachParentFirst(
|
|
_attachedByChild,
|
|
_parentOfAttached,
|
|
_tickAttached);
|
|
for (int i = 0; i < failed.Count; i++)
|
|
WithdrawForPoseLoss(failed[i]);
|
|
}
|
|
|
|
private void RetryPendingProjectionTransitions()
|
|
{
|
|
foreach ((uint rootGuid, PendingOrdinaryRemoval pending) in
|
|
_pendingOrdinaryRemovalByRoot.ToArray())
|
|
{
|
|
AdvanceOrdinaryRemoval(rootGuid, pending);
|
|
}
|
|
|
|
foreach ((uint childGuid, PendingProjectionSubtree pending) in
|
|
_pendingDetachedRemovalByChild.ToArray())
|
|
{
|
|
AdvanceDetachedRemoval(childGuid, pending);
|
|
}
|
|
|
|
RetryProjectionSubtrees(_pendingReparentRemovalByChild);
|
|
RetryProjectionSubtrees(_pendingPoseLossRemovalByChild);
|
|
RetryProjectionSubtrees(_pendingOrphanRemovalByChild);
|
|
|
|
foreach ((uint childGuid, PendingUnparentTransition pending) in
|
|
_pendingUnparentByChild.ToArray())
|
|
{
|
|
if (!_liveEntities.IsCurrentRecord(pending.Record)
|
|
|| pending.Record.PositionAuthorityVersion
|
|
!= pending.PositionAuthorityVersion)
|
|
{
|
|
_pendingUnparentByChild.Remove(childGuid);
|
|
continue;
|
|
}
|
|
AdvanceUnparentTransition(childGuid, pending);
|
|
}
|
|
|
|
Relations.CopyPendingProjectionChildrenTo(_pendingProjectionChildrenScratch);
|
|
for (int i = 0; i < _pendingProjectionChildrenScratch.Count; i++)
|
|
ResolveAndTryRealize(_pendingProjectionChildrenScratch[i]);
|
|
}
|
|
|
|
private bool TickChild(uint childGuid)
|
|
{
|
|
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child))
|
|
return false;
|
|
|
|
if (TryResolveExactAttachment(child, out WorldEntity parent)
|
|
&& _poses.TryGetRootPose(parent.Id, out Matrix4x4 parentWorld)
|
|
&& _poses.TryGetPartPoseSnapshot(
|
|
parent.Id,
|
|
out var parentPartPoses,
|
|
out var parentPartAvailability)
|
|
&& EquippedChildAttachment.TryComposePoseInto(
|
|
child.ParentSetup,
|
|
parentPartPoses,
|
|
parentPartAvailability,
|
|
child.ChildSetup,
|
|
child.ParentLocation,
|
|
child.Placement,
|
|
child.PartTemplate,
|
|
child.Scale,
|
|
child.PartPoseBuffer,
|
|
child.AttachedPartBuffer,
|
|
out EquippedChildPose pose))
|
|
{
|
|
child.Entity.MeshRefs = pose.AttachedParts;
|
|
child.Entity.SetIndexedPartPoses(pose.PartLocal, child.PartAvailability);
|
|
if (!ApplyParentWorldPose(child.Entity, parentWorld))
|
|
return false;
|
|
ApplyParentDrawVisibility(child.Entity, parent);
|
|
child.Entity.ParentCellId = parent.ParentCellId;
|
|
PublishChildPose(child.Entity, parentWorld, parent.ParentCellId, pose);
|
|
if (TryResolveExactAttachment(child, out parent)
|
|
&& parent.ParentCellId is { } parentCellId)
|
|
_liveEntities.RebucketLiveEntity(child.ChildGuid, parentCellId);
|
|
ProjectionPoseReady?.Invoke(child.ChildGuid);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private bool TryRealize(
|
|
ParentAttachmentRelation pending,
|
|
ParentProjectionCandidateKind candidateKind)
|
|
{
|
|
uint childGuid = pending.ChildGuid;
|
|
|
|
if (!_liveEntities.TryGetRecord(pending.ParentGuid, out LiveEntityRecord parentRecord)
|
|
|| parentRecord.WorldEntity is not { } parentEntity
|
|
|| !parentRecord.IsSpatiallyProjected
|
|
|| !_liveEntities.TryGetRecord(childGuid, out LiveEntityRecord childRecord)
|
|
|| !_liveEntities.TryGetSnapshot(pending.ParentGuid, out WorldSession.EntitySpawn parentSpawn)
|
|
|| !_liveEntities.TryGetSnapshot(childGuid, out WorldSession.EntitySpawn childSpawn))
|
|
return false;
|
|
|
|
if (parentSpawn.SetupTableId is not { } parentSetupId
|
|
|| childSpawn.SetupTableId is not { } childSetupId
|
|
|| parentEntity.ParentCellId is not { } parentCellId)
|
|
return false;
|
|
|
|
Setup? parentSetup = _dats.Get<Setup>(parentSetupId);
|
|
Setup? childSetup = _dats.Get<Setup>(childSetupId);
|
|
if (parentSetup is null || childSetup is null)
|
|
return false;
|
|
|
|
var parentLocation = (ParentLocation)pending.ParentLocation;
|
|
var placement = (Placement)pending.PlacementId;
|
|
IReadOnlyList<MeshRef> template = BuildPartTemplate(childSetup, childSpawn);
|
|
bool[] childPartAvailability = BuildPartAvailability(template);
|
|
float scale = childSpawn.ObjScale is { } objScale && objScale > 0f
|
|
? objScale
|
|
: 1.0f;
|
|
if (!_poses.TryGetPartPoseSnapshot(
|
|
parentEntity.Id,
|
|
out var parentPartPoses,
|
|
out var parentPartAvailability))
|
|
return false;
|
|
if (!_poses.TryGetRootPose(parentEntity.Id, out Matrix4x4 parentWorld)
|
|
|| !TryDecomposeWorldPose(parentWorld, out Vector3 parentPosition, out Quaternion parentRotation))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!EquippedChildAttachment.TryComposePoseInto(
|
|
parentSetup,
|
|
parentPartPoses,
|
|
parentPartAvailability,
|
|
childSetup,
|
|
parentLocation,
|
|
placement,
|
|
template,
|
|
scale,
|
|
partPoseBuffer: null,
|
|
attachedPartBuffer: null,
|
|
out EquippedChildPose pose))
|
|
return false;
|
|
|
|
// A parented object may materialize here before the ordinary world
|
|
// hydration path sees it. Install its logical effect profile before
|
|
// MaterializeLiveEntity registers runtime resources, exactly as for a
|
|
// top-level projection; rebucketing/reattachment must never recreate it.
|
|
if (!_liveEntities.TryGetEffectProfile(childGuid, out _))
|
|
{
|
|
var effectProfile = childSpawn.Physics is { } physics
|
|
? Vfx.EntityEffectProfile.CreateLive(childSetup, physics)
|
|
: Vfx.EntityEffectProfile.CreateDatStatic(childSetup);
|
|
_liveEntities.SetEffectProfile(childGuid, effectProfile);
|
|
}
|
|
|
|
if (!_liveEntities.IsCurrentRecord(parentRecord)
|
|
|| !_liveEntities.IsCurrentRecord(childRecord)
|
|
|| !Relations.IsPending(pending, candidateKind))
|
|
{
|
|
return false;
|
|
}
|
|
WorldEntity? entity = _liveEntities.MaterializeLiveEntity(
|
|
childGuid,
|
|
parentCellId,
|
|
localId =>
|
|
{
|
|
var created = new WorldEntity
|
|
{
|
|
Id = localId,
|
|
ServerGuid = childGuid,
|
|
SourceGfxObjOrSetupId = childSetupId,
|
|
Position = parentPosition,
|
|
Rotation = parentRotation,
|
|
MeshRefs = pose.AttachedParts,
|
|
PaletteOverride = BuildPaletteOverride(childSpawn),
|
|
ParentCellId = parentCellId,
|
|
};
|
|
created.SetIndexedPartPoses(pose.PartLocal, childPartAvailability);
|
|
return created;
|
|
},
|
|
LiveEntityProjectionKind.Attached);
|
|
if (entity is null)
|
|
return false;
|
|
if (!_liveEntities.IsCurrentRecord(parentRecord)
|
|
|| !_liveEntities.IsCurrentRecord(childRecord)
|
|
|| !ReferenceEquals(childRecord.WorldEntity, entity)
|
|
|| !Relations.IsPending(pending, candidateKind))
|
|
{
|
|
if (_liveEntities.IsCurrentRecord(childRecord)
|
|
&& ReferenceEquals(childRecord.WorldEntity, entity)
|
|
&& childRecord.IsSpatiallyProjected)
|
|
{
|
|
BeginProjectionSubtreeWithdrawal(
|
|
_pendingOrphanRemovalByChild,
|
|
childRecord,
|
|
restoreRootRelation: false,
|
|
restoreDescendantRelations: false);
|
|
}
|
|
return false;
|
|
}
|
|
childRecord.HasPartArray = true;
|
|
ApplyParentWorldPose(entity, parentWorld);
|
|
ApplyParentDrawVisibility(entity, parentEntity);
|
|
entity.ParentCellId = parentCellId;
|
|
entity.ApplyAppearance(
|
|
pose.AttachedParts,
|
|
BuildPaletteOverride(childSpawn),
|
|
childSpawn.AnimPartChanges is { Count: > 0 } changes
|
|
? changes.Select(change => new PartOverride(change.PartIndex, change.NewModelId)).ToArray()
|
|
: Array.Empty<PartOverride>());
|
|
entity.SetIndexedPartPoses(pose.PartLocal, childPartAvailability);
|
|
_attachedByChild[childGuid] = new AttachedChild(
|
|
parentRecord,
|
|
childRecord,
|
|
pending.ParentGuid,
|
|
childGuid,
|
|
parentLocation,
|
|
placement,
|
|
parentSetup,
|
|
childSetup,
|
|
template,
|
|
childPartAvailability,
|
|
pose.PartLocal,
|
|
pose.AttachedParts,
|
|
scale,
|
|
entity);
|
|
_pendingUnparentByChild.Remove(childGuid);
|
|
if ((parentRecord.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0)
|
|
{
|
|
// A child attached while its parent is already Hidden inherits
|
|
// the same direct child NoDraw mutation retail applies from
|
|
// CPhysicsObj::set_hidden (0x00514C60).
|
|
_liveEntities.SetAttachedChildNoDraw(childGuid, noDraw: true);
|
|
}
|
|
PublishChildPose(entity, parentWorld, parentEntity.ParentCellId, pose);
|
|
Console.WriteLine(
|
|
$"equipment: attached child=0x{childGuid:X8} parent=0x{pending.ParentGuid:X8} " +
|
|
$"location={parentLocation} placement={placement}");
|
|
Relations.MarkProjected(pending, candidateKind);
|
|
LiveEntityReadyCandidate readyCandidate =
|
|
LiveEntityReadyCandidate.Capture(childRecord);
|
|
ProjectionPoseReady?.Invoke(childGuid);
|
|
return PublishEntityReadyExact(
|
|
_liveEntities,
|
|
readyCandidate,
|
|
EntityReady);
|
|
}
|
|
|
|
internal static bool PublishEntityReadyExact(
|
|
LiveEntityRuntime runtime,
|
|
LiveEntityReadyCandidate candidate,
|
|
Action<LiveEntityReadyCandidate>? publish)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(runtime);
|
|
if (!candidate.IsCurrent(runtime))
|
|
return false;
|
|
|
|
publish?.Invoke(candidate);
|
|
return candidate.IsCurrent(runtime);
|
|
}
|
|
|
|
private void PublishChildPose(
|
|
WorldEntity child,
|
|
Matrix4x4 parentWorld,
|
|
uint? parentCellId,
|
|
EquippedChildPose pose)
|
|
{
|
|
_poses.Publish(
|
|
child.Id,
|
|
pose.RootLocal * parentWorld,
|
|
pose.PartLocal,
|
|
parentCellId ?? 0u,
|
|
child.IndexedPartAvailable);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Installs the immediate parent's composed world root on the child
|
|
/// projection. The child's MeshRefs already contain its holding and
|
|
/// placement transforms relative to that direct parent root.
|
|
/// </summary>
|
|
internal static bool ApplyParentWorldPose(WorldEntity child, Matrix4x4 parentWorld)
|
|
{
|
|
if (!TryDecomposeWorldPose(parentWorld, out Vector3 position, out Quaternion rotation))
|
|
return false;
|
|
child.SetPosition(position);
|
|
child.Rotation = rotation;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Preserve retail attachment-tree draw inheritance after adapting every
|
|
/// child CPhysicsObj into an independent retained draw entry. This does not
|
|
/// alter the child's own NoDraw state; parent-first ticking propagates an
|
|
/// ancestor's suppression through arbitrarily deep attachment chains.
|
|
/// </summary>
|
|
internal static void ApplyParentDrawVisibility(WorldEntity child, WorldEntity parent)
|
|
{
|
|
child.IsAncestorDrawVisible =
|
|
parent.IsDrawVisible && parent.IsAncestorDrawVisible;
|
|
}
|
|
|
|
private static bool TryDecomposeWorldPose(
|
|
Matrix4x4 world,
|
|
out Vector3 position,
|
|
out Quaternion rotation)
|
|
{
|
|
position = world.Translation;
|
|
if (!Matrix4x4.Decompose(world, out Vector3 scale, out rotation, out _)
|
|
|| Vector3.DistanceSquared(scale, Vector3.One) > 1e-6f)
|
|
return false;
|
|
rotation = Quaternion.Normalize(rotation);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves retail's parent Setup part index to the currently attached
|
|
/// child local ID for DefaultScriptPartHook.
|
|
/// </summary>
|
|
public uint? FindChildLocalIdAtPart(uint parentLocalId, uint partIndex)
|
|
{
|
|
foreach (AttachedChild child in _attachedByChild.Values)
|
|
{
|
|
if (!TryResolveExactAttachment(child, out WorldEntity parent)
|
|
|| parent.Id != parentLocalId
|
|
|| !child.ParentSetup.HoldingLocations.TryGetValue(
|
|
child.ParentLocation,
|
|
out LocationType? holding)
|
|
|| holding.PartId != partIndex)
|
|
{
|
|
continue;
|
|
}
|
|
return child.Entity.Id;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Returns the live parent whose UpdateChild path owns this child.</summary>
|
|
public uint? FindParentLocalId(uint childLocalId)
|
|
{
|
|
foreach (AttachedChild child in _attachedByChild.Values)
|
|
{
|
|
if (child.Entity.Id != childLocalId)
|
|
continue;
|
|
if (TryResolveExactAttachment(child, out WorldEntity parent))
|
|
return parent.Id;
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail <c>CPhysicsObj::set_hidden</c> walks the direct CHILDLIST and
|
|
/// sets/clears each child's NoDraw bit before changing root collision/cell
|
|
/// presentation. Descendants are not recursively rewritten here.
|
|
/// </summary>
|
|
public void SetDirectChildrenNoDraw(uint parentGuid, bool noDraw)
|
|
{
|
|
foreach (AttachedChild child in _attachedByChild.Values)
|
|
{
|
|
if (child.ParentGuid == parentGuid
|
|
&& TryResolveExactAttachment(child, out _))
|
|
_liveEntities.SetAttachedChildNoDraw(child.ChildGuid, noDraw);
|
|
}
|
|
}
|
|
|
|
public void OnCreateParentAccepted(CreateParentUpdate update)
|
|
{
|
|
lock (_datLock)
|
|
{
|
|
Relations.AcceptCreateObjectRelation(new ParentAttachmentRelation(
|
|
update.ParentGuid,
|
|
update.ChildGuid,
|
|
update.ParentLocation,
|
|
update.PlacementId,
|
|
ParentInstanceSequence: 0,
|
|
update.ChildPositionSequence));
|
|
ResolveAndTryRealize(update.ChildGuid);
|
|
}
|
|
}
|
|
|
|
private bool TryResolveExactAttachment(
|
|
AttachedChild child,
|
|
out WorldEntity parent)
|
|
{
|
|
if (_liveEntities.IsCurrentRecord(child.ParentRecord)
|
|
&& _liveEntities.IsCurrentRecord(child.ChildRecord)
|
|
&& child.ParentRecord.IsSpatiallyProjected
|
|
&& child.ChildRecord.IsSpatiallyProjected
|
|
&& child.ParentRecord.WorldEntity is { } exactParent
|
|
&& child.ChildRecord.WorldEntity is { } exactChild
|
|
&& ReferenceEquals(exactChild, child.Entity))
|
|
{
|
|
parent = exactParent;
|
|
return true;
|
|
}
|
|
parent = null!;
|
|
return false;
|
|
}
|
|
|
|
private void ResolveRelations(uint childGuid)
|
|
{
|
|
Relations.Resolve(
|
|
childGuid,
|
|
guid => _liveEntities.TryGetSnapshot(guid, out _),
|
|
guid => _liveEntities.TryGetSnapshot(guid, out WorldSession.EntitySpawn spawn)
|
|
? spawn.InstanceSequence
|
|
: null,
|
|
_acceptParent);
|
|
}
|
|
|
|
private bool ResolveAndTryRealize(uint childGuid)
|
|
{
|
|
bool projected = false;
|
|
while (true)
|
|
{
|
|
ResolveRelations(childGuid);
|
|
if (!Relations.TryGetStagedProjection(
|
|
childGuid,
|
|
out ParentAttachmentRelation staged))
|
|
{
|
|
break;
|
|
}
|
|
|
|
ParentProjectionValidationDisposition validation =
|
|
ValidateParentProjection(staged);
|
|
if (validation is ParentProjectionValidationDisposition.Waiting)
|
|
break;
|
|
if (validation is ParentProjectionValidationDisposition.Rejected)
|
|
{
|
|
Relations.RejectProjection(staged);
|
|
continue;
|
|
}
|
|
|
|
ProjectionPreparationResult result = PrepareAndTryRealize(
|
|
staged,
|
|
ParentProjectionCandidateKind.Staged);
|
|
projected |= result.Projected;
|
|
if (!result.CanAdvanceWireQueue)
|
|
return projected;
|
|
}
|
|
|
|
if (Relations.TryGetRecoveryProjection(
|
|
childGuid,
|
|
out ParentAttachmentRelation recovery)
|
|
&& ValidateParentProjection(recovery)
|
|
is ParentProjectionValidationDisposition.Ready)
|
|
{
|
|
projected |= PrepareAndTryRealize(
|
|
recovery,
|
|
ParentProjectionCandidateKind.Recovery).Projected;
|
|
}
|
|
return projected;
|
|
}
|
|
|
|
private ProjectionPreparationResult PrepareAndTryRealize(
|
|
ParentAttachmentRelation relation,
|
|
ParentProjectionCandidateKind candidateKind)
|
|
{
|
|
if (!_liveEntities.TryGetRecord(
|
|
relation.ChildGuid,
|
|
out LiveEntityRecord childRecord))
|
|
{
|
|
return default;
|
|
}
|
|
|
|
ulong positionAuthorityVersion = childRecord.PositionAuthorityVersion;
|
|
if (candidateKind is ParentProjectionCandidateKind.Staged)
|
|
{
|
|
if (!_liveEntities.CommitStagedParent(relation, out _)
|
|
|| !Relations.CommitProjection(relation))
|
|
{
|
|
return default;
|
|
}
|
|
candidateKind = ParentProjectionCandidateKind.Recovery;
|
|
}
|
|
else if (!Relations.IsCommitted(relation))
|
|
{
|
|
return default;
|
|
}
|
|
|
|
if (!Relations.IsPending(relation, candidateKind)
|
|
|| !_liveEntities.CommitAcceptedParentCellless(
|
|
childRecord,
|
|
positionAuthorityVersion)
|
|
|| !WithdrawPriorProjection(childRecord))
|
|
{
|
|
return default;
|
|
}
|
|
return new(
|
|
CanAdvanceWireQueue: true,
|
|
Projected: TryRealize(relation, candidateKind));
|
|
}
|
|
|
|
internal ParentProjectionValidationDisposition ValidateParentProjection(
|
|
ParentAttachmentRelation relation)
|
|
{
|
|
if (relation.ParentGuid == relation.ChildGuid)
|
|
return ParentProjectionValidationDisposition.Rejected;
|
|
if (!_liveEntities.TryGetRecord(relation.ParentGuid, out LiveEntityRecord parent)
|
|
|| !_liveEntities.TryGetRecord(relation.ChildGuid, out _)
|
|
|| parent.WorldEntity is null
|
|
|| !parent.HasPartArray)
|
|
{
|
|
return ParentProjectionValidationDisposition.Waiting;
|
|
}
|
|
if (!_liveEntities.TryGetSnapshot(
|
|
relation.ParentGuid,
|
|
out WorldSession.EntitySpawn parentSpawn)
|
|
|| parentSpawn.SetupTableId is not { } parentSetupId)
|
|
{
|
|
return ParentProjectionValidationDisposition.Rejected;
|
|
}
|
|
Setup? parentSetup = _dats.Get<Setup>(parentSetupId);
|
|
return parentSetup is not null
|
|
&& parentSetup.HoldingLocations.ContainsKey(
|
|
(ParentLocation)relation.ParentLocation)
|
|
? ParentProjectionValidationDisposition.Ready
|
|
: ParentProjectionValidationDisposition.Rejected;
|
|
}
|
|
|
|
private readonly record struct ProjectionPreparationResult(
|
|
bool CanAdvanceWireQueue,
|
|
bool Projected);
|
|
|
|
private void RetryWaitingDescendants(uint parentGuid)
|
|
{
|
|
_updateOrder.RealizeDescendants(
|
|
parentGuid,
|
|
Relations.ChildrenWaitingForParent,
|
|
ResolveAndTryRealize);
|
|
}
|
|
|
|
private IReadOnlyList<MeshRef> BuildPartTemplate(
|
|
Setup setup,
|
|
WorldSession.EntitySpawn spawn)
|
|
{
|
|
var result = new MeshRef[setup.Parts.Count];
|
|
for (int i = 0; i < result.Length; i++)
|
|
result[i] = new MeshRef((uint)setup.Parts[i], Matrix4x4.Identity);
|
|
|
|
IReadOnlyList<CreateObject.AnimPartChange> partChanges =
|
|
spawn.AnimPartChanges ?? Array.Empty<CreateObject.AnimPartChange>();
|
|
for (int i = 0; i < partChanges.Count; i++)
|
|
{
|
|
CreateObject.AnimPartChange change = partChanges[i];
|
|
if (change.PartIndex < result.Length)
|
|
result[change.PartIndex] = new MeshRef(change.NewModelId, Matrix4x4.Identity);
|
|
}
|
|
|
|
IReadOnlyList<CreateObject.TextureChange> textureChanges =
|
|
spawn.TextureChanges ?? Array.Empty<CreateObject.TextureChange>();
|
|
for (int partIndex = 0; partIndex < result.Length; partIndex++)
|
|
{
|
|
Dictionary<uint, uint>? oldToNew = null;
|
|
for (int t = 0; t < textureChanges.Count; t++)
|
|
{
|
|
CreateObject.TextureChange change = textureChanges[t];
|
|
if (change.PartIndex != partIndex) continue;
|
|
oldToNew ??= new Dictionary<uint, uint>();
|
|
oldToNew[change.OldTexture] = change.NewTexture;
|
|
}
|
|
if (oldToNew is null) continue;
|
|
|
|
GfxObj? gfx = _dats.Get<GfxObj>(result[partIndex].GfxObjId);
|
|
if (gfx is null) continue;
|
|
Dictionary<uint, uint>? surfaceOverrides = null;
|
|
foreach (var surfaceQid in gfx.Surfaces)
|
|
{
|
|
uint surfaceId = (uint)surfaceQid;
|
|
Surface? surface = _dats.Get<Surface>(surfaceId);
|
|
if (surface is null) continue;
|
|
uint oldTexture = (uint)surface.OrigTextureId;
|
|
if (!oldToNew.TryGetValue(oldTexture, out uint replacement)) continue;
|
|
surfaceOverrides ??= new Dictionary<uint, uint>();
|
|
surfaceOverrides[surfaceId] = replacement;
|
|
}
|
|
|
|
if (surfaceOverrides is not null)
|
|
{
|
|
result[partIndex] = new MeshRef(
|
|
result[partIndex].GfxObjId,
|
|
Matrix4x4.Identity)
|
|
{
|
|
SurfaceOverrides = surfaceOverrides,
|
|
};
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private bool[] BuildPartAvailability(IReadOnlyList<MeshRef> template)
|
|
{
|
|
var available = new bool[template.Count];
|
|
for (int i = 0; i < template.Count; i++)
|
|
available[i] = _dats.Get<GfxObj>(template[i].GfxObjId) is not null;
|
|
return available;
|
|
}
|
|
|
|
private static PaletteOverride? BuildPaletteOverride(WorldSession.EntitySpawn spawn)
|
|
{
|
|
if (spawn.SubPalettes is not { Count: > 0 } subPalettes)
|
|
return null;
|
|
|
|
var ranges = new PaletteOverride.SubPaletteRange[subPalettes.Count];
|
|
for (int i = 0; i < subPalettes.Count; i++)
|
|
{
|
|
CreateObject.SubPaletteSwap swap = subPalettes[i];
|
|
ranges[i] = new PaletteOverride.SubPaletteRange(
|
|
swap.SubPaletteId,
|
|
swap.Offset,
|
|
swap.Length);
|
|
}
|
|
return new PaletteOverride(spawn.BasePaletteId ?? 0, ranges);
|
|
}
|
|
|
|
private void OnObjectMoved(ClientObjectMove move)
|
|
{
|
|
if (move.Current.EquipLocation == EquipMask.None)
|
|
BeginDetachedRemoval(move.ItemId);
|
|
}
|
|
|
|
private void BeginDetachedRemoval(uint childGuid)
|
|
{
|
|
if (!_liveEntities.TryGetRecord(childGuid, out LiveEntityRecord record))
|
|
return;
|
|
BeginProjectionSubtreeWithdrawal(
|
|
_pendingDetachedRemovalByChild,
|
|
record,
|
|
restoreRootRelation: false,
|
|
restoreDescendantRelations: true);
|
|
}
|
|
|
|
private void AdvanceDetachedRemoval(
|
|
uint childGuid,
|
|
PendingProjectionSubtree pending)
|
|
{
|
|
AdvanceProjectionSubtree(
|
|
_pendingDetachedRemovalByChild,
|
|
childGuid,
|
|
pending);
|
|
}
|
|
|
|
private void OnMoveRolledBack(ClientObject item)
|
|
{
|
|
if (item.CurrentlyEquippedLocation == EquipMask.None)
|
|
return;
|
|
if (_attachedByChild.TryGetValue(item.ObjectId, out AttachedChild? attached)
|
|
&& _liveEntities.IsCurrentRecord(attached.ChildRecord)
|
|
&& attached.ChildRecord.IsSpatiallyProjected)
|
|
{
|
|
// A component-stage failure left the exact equipped projection
|
|
// untouched. The authoritative rollback therefore has nothing to
|
|
// reconstruct and merely cancels its retained leave-world retry.
|
|
_pendingDetachedRemovalByChild.Remove(item.ObjectId);
|
|
return;
|
|
}
|
|
if (_pendingDetachedRemovalByChild.TryGetValue(
|
|
item.ObjectId,
|
|
out PendingProjectionSubtree pending))
|
|
{
|
|
AdvanceDetachedRemoval(item.ObjectId, pending);
|
|
if (_pendingDetachedRemovalByChild.ContainsKey(item.ObjectId))
|
|
return;
|
|
}
|
|
|
|
// A rejected unwield restores the equipped location without a fresh
|
|
// wire ParentEvent; reinstall the last accepted relationship only for
|
|
// this explicit rollback signal.
|
|
if (Relations.RestoreLastAccepted(item.ObjectId))
|
|
{
|
|
lock (_datLock)
|
|
{
|
|
if (ResolveAndTryRealize(item.ObjectId))
|
|
RetryWaitingDescendants(item.ObjectId);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool Remove(uint childGuid)
|
|
{
|
|
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child))
|
|
return false;
|
|
if (!_liveEntities.IsCurrentRecord(child.ChildRecord))
|
|
return CommitProjectionRemoval(child);
|
|
return Remove(
|
|
child,
|
|
child.ChildRecord.PositionAuthorityVersion,
|
|
child.ChildRecord.ProjectionMutationVersion);
|
|
}
|
|
|
|
private bool Remove(
|
|
AttachedChild child,
|
|
ulong positionAuthorityVersion,
|
|
ulong projectionMutationVersion)
|
|
{
|
|
if (!_liveEntities.IsCurrentRecord(child.ChildRecord))
|
|
return CommitProjectionRemoval(child);
|
|
ExactProjectionWithdrawalOutcome outcome = WithdrawAttachedProjection(
|
|
child.ChildRecord,
|
|
positionAuthorityVersion,
|
|
projectionMutationVersion,
|
|
_withdrawProjection,
|
|
() => CommitProjectionRemoval(child));
|
|
if (outcome.Failure is not null)
|
|
throw outcome.Failure;
|
|
return outcome.Disposition is ExactProjectionWithdrawalDisposition.Completed;
|
|
}
|
|
|
|
private bool WithdrawPriorProjection(LiveEntityRecord childRecord)
|
|
{
|
|
if (_pendingReparentRemovalByChild.TryGetValue(
|
|
childRecord.ServerGuid,
|
|
out PendingProjectionSubtree pending))
|
|
{
|
|
return AdvanceProjectionSubtree(
|
|
_pendingReparentRemovalByChild,
|
|
childRecord.ServerGuid,
|
|
pending);
|
|
}
|
|
return BeginProjectionSubtreeWithdrawal(
|
|
_pendingReparentRemovalByChild,
|
|
childRecord,
|
|
restoreRootRelation: false,
|
|
restoreDescendantRelations: true);
|
|
}
|
|
|
|
private ChildUnparentDisposition AdvanceUnparentTransition(
|
|
uint childGuid,
|
|
PendingUnparentTransition pending)
|
|
{
|
|
ExactProjectionWithdrawalOutcome outcome = AdvanceProjectionSubtree(
|
|
pending.Subtree,
|
|
out PendingProjectionSubtree next);
|
|
|
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending)
|
|
{
|
|
_pendingUnparentByChild[childGuid] = pending with { Subtree = next };
|
|
if (outcome.Failure is not null)
|
|
throw outcome.Failure;
|
|
return ChildUnparentDisposition.Pending;
|
|
}
|
|
|
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Superseded)
|
|
{
|
|
_pendingUnparentByChild.Remove(childGuid);
|
|
if (outcome.Failure is not null)
|
|
throw outcome.Failure;
|
|
return ChildUnparentDisposition.Superseded;
|
|
}
|
|
|
|
PendingUnparentTransition awaitingContinuation = pending with { Subtree = next };
|
|
_pendingUnparentByChild[childGuid] = awaitingContinuation;
|
|
if (outcome.Failure is not null)
|
|
throw outcome.Failure;
|
|
|
|
Relations.EndChildProjection(childGuid);
|
|
try
|
|
{
|
|
awaitingContinuation.Continuation?.Invoke();
|
|
_pendingUnparentByChild.Remove(childGuid);
|
|
return ChildUnparentDisposition.Completed;
|
|
}
|
|
catch
|
|
{
|
|
bool continuationCommitted =
|
|
_liveEntities.IsCurrentRecord(awaitingContinuation.Record)
|
|
&& awaitingContinuation.Record.PositionAuthorityVersion
|
|
== awaitingContinuation.PositionAuthorityVersion
|
|
&& awaitingContinuation.Record.IsSpatiallyProjected
|
|
&& awaitingContinuation.Record.ProjectionKind
|
|
is LiveEntityProjectionKind.World;
|
|
if (continuationCommitted)
|
|
_pendingUnparentByChild.Remove(childGuid);
|
|
else
|
|
_pendingUnparentByChild[childGuid] = awaitingContinuation;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
internal static ExactProjectionWithdrawalOutcome WithdrawAttachedProjection(
|
|
LiveEntityRecord childRecord,
|
|
ulong positionAuthorityVersion,
|
|
ulong projectionMutationVersion,
|
|
Func<LiveEntityRecord, ulong, ulong, ExactProjectionWithdrawalOutcome>
|
|
withdrawProjection,
|
|
Func<bool> commitRemoval)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(childRecord);
|
|
ArgumentNullException.ThrowIfNull(withdrawProjection);
|
|
ArgumentNullException.ThrowIfNull(commitRemoval);
|
|
ExactProjectionWithdrawalOutcome outcome = withdrawProjection(
|
|
childRecord,
|
|
positionAuthorityVersion,
|
|
projectionMutationVersion);
|
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending)
|
|
return outcome;
|
|
try
|
|
{
|
|
commitRemoval();
|
|
return outcome;
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
return new ExactProjectionWithdrawalOutcome(
|
|
outcome.Disposition,
|
|
outcome.Failure is null
|
|
? error
|
|
: new AggregateException(outcome.Failure, error));
|
|
}
|
|
}
|
|
|
|
private bool CommitProjectionRemoval(AttachedChild child)
|
|
{
|
|
if (!_attachedByChild.TryGetValue(child.ChildGuid, out AttachedChild? current)
|
|
|| !ReferenceEquals(current, child))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_attachedByChild.Remove(child.ChildGuid);
|
|
ProjectionRemoved?.Invoke(child.Entity.Id);
|
|
return true;
|
|
}
|
|
|
|
private void WithdrawForPoseLoss(uint childGuid)
|
|
{
|
|
if (!_liveEntities.TryGetRecord(childGuid, out LiveEntityRecord record))
|
|
return;
|
|
BeginProjectionSubtreeWithdrawal(
|
|
_pendingPoseLossRemovalByChild,
|
|
record,
|
|
restoreRootRelation: true,
|
|
restoreDescendantRelations: true);
|
|
}
|
|
|
|
private void TearDownCurrentObjectProjections(uint guid)
|
|
{
|
|
if (_liveEntities.TryGetRecord(guid, out LiveEntityRecord record))
|
|
{
|
|
List<AttachedRemovalCapture> captures = CaptureRecordSubtreeParentFirst(record);
|
|
AdvanceOrdinaryRemoval(
|
|
guid,
|
|
new PendingOrdinaryRemoval(record, captures, NextIndex: 0));
|
|
return;
|
|
}
|
|
|
|
if (_attachedByChild.TryGetValue(guid, out AttachedChild? stale))
|
|
CommitProjectionRemoval(stale);
|
|
Relations.EndChildProjection(guid);
|
|
}
|
|
|
|
private void AdvanceOrdinaryRemoval(
|
|
uint rootGuid,
|
|
PendingOrdinaryRemoval pending)
|
|
{
|
|
if (!_liveEntities.IsCurrentRecord(pending.RootRecord))
|
|
{
|
|
_pendingOrdinaryRemovalByRoot.Remove(rootGuid);
|
|
return;
|
|
}
|
|
|
|
for (int i = pending.NextIndex; i < pending.Captures.Count; i++)
|
|
{
|
|
AttachedRemovalCapture captured = pending.Captures[i];
|
|
ExactProjectionWithdrawalOutcome outcome = WithdrawCaptured(captured);
|
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending)
|
|
{
|
|
_pendingOrdinaryRemovalByRoot[rootGuid] = pending with { NextIndex = i };
|
|
if (outcome.Failure is not null)
|
|
throw outcome.Failure;
|
|
return;
|
|
}
|
|
|
|
if (!ReferenceEquals(captured.Attached.ChildRecord, pending.RootRecord)
|
|
&& outcome.Disposition is ExactProjectionWithdrawalDisposition.Completed)
|
|
{
|
|
Relations.RestoreLastAccepted(captured.Attached.ChildGuid);
|
|
}
|
|
|
|
if (outcome.Failure is not null)
|
|
{
|
|
_pendingOrdinaryRemovalByRoot[rootGuid] = pending with
|
|
{
|
|
NextIndex = i + 1,
|
|
};
|
|
throw outcome.Failure;
|
|
}
|
|
}
|
|
|
|
_pendingOrdinaryRemovalByRoot.Remove(rootGuid);
|
|
Relations.EndChildProjection(rootGuid);
|
|
}
|
|
|
|
private void TearDownRecordProjections(LiveEntityRecord record)
|
|
{
|
|
if (_pendingUnparentByChild.TryGetValue(record.ServerGuid, out var pending)
|
|
&& ReferenceEquals(pending.Record, record))
|
|
{
|
|
_pendingUnparentByChild.Remove(record.ServerGuid);
|
|
}
|
|
if (_pendingOrdinaryRemovalByRoot.TryGetValue(
|
|
record.ServerGuid,
|
|
out PendingOrdinaryRemoval ordinary)
|
|
&& ReferenceEquals(ordinary.RootRecord, record))
|
|
{
|
|
_pendingOrdinaryRemovalByRoot.Remove(record.ServerGuid);
|
|
}
|
|
RemovePendingProjectionSubtree(_pendingDetachedRemovalByChild, record);
|
|
RemovePendingProjectionSubtree(_pendingReparentRemovalByChild, record);
|
|
RemovePendingProjectionSubtree(_pendingPoseLossRemovalByChild, record);
|
|
RemovePendingProjectionSubtree(_pendingOrphanRemovalByChild, record);
|
|
List<AttachedRemovalCapture> subtree = CaptureRecordSubtreeParentFirst(record);
|
|
for (int i = 0; i < subtree.Count; i++)
|
|
{
|
|
AttachedRemovalCapture captured = subtree[i];
|
|
AttachedChild child = captured.Attached;
|
|
if (ReferenceEquals(child.ChildRecord, record))
|
|
{
|
|
CommitProjectionRemoval(child);
|
|
continue;
|
|
}
|
|
ExactProjectionWithdrawalOutcome outcome = WithdrawCaptured(captured);
|
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending)
|
|
{
|
|
if (outcome.Failure is not null)
|
|
throw outcome.Failure;
|
|
throw new InvalidOperationException(
|
|
$"Attached projection 0x{child.ChildGuid:X8} remains pending exact teardown of 0x{record.ServerGuid:X8}.");
|
|
}
|
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Completed)
|
|
Relations.RestoreLastAccepted(child.ChildGuid);
|
|
if (outcome.Failure is not null)
|
|
throw outcome.Failure;
|
|
}
|
|
}
|
|
|
|
private List<AttachedRemovalCapture> CaptureRecordSubtreeParentFirst(
|
|
LiveEntityRecord record)
|
|
{
|
|
var result = new List<AttachedRemovalCapture>();
|
|
var visited = new HashSet<LiveEntityRecord>(ReferenceEqualityComparer.Instance);
|
|
if (_attachedByChild.TryGetValue(record.ServerGuid, out AttachedChild? root)
|
|
&& ReferenceEquals(root.ChildRecord, record))
|
|
{
|
|
result.Add(Capture(root));
|
|
}
|
|
CollectDescendantsParentFirst(record, result, visited);
|
|
return result;
|
|
}
|
|
|
|
private void CollectDescendantsParentFirst(
|
|
LiveEntityRecord parent,
|
|
List<AttachedRemovalCapture> destination,
|
|
HashSet<LiveEntityRecord> visited)
|
|
{
|
|
if (!visited.Add(parent))
|
|
return;
|
|
AttachedChild[] children = _attachedByChild.Values
|
|
.Where(child => ReferenceEquals(child.ParentRecord, parent))
|
|
.ToArray();
|
|
for (int i = 0; i < children.Length; i++)
|
|
{
|
|
destination.Add(Capture(children[i]));
|
|
CollectDescendantsParentFirst(children[i].ChildRecord, destination, visited);
|
|
}
|
|
}
|
|
|
|
private static AttachedRemovalCapture Capture(AttachedChild child) => new(
|
|
child,
|
|
child.ChildRecord.PositionAuthorityVersion,
|
|
child.ChildRecord.ProjectionMutationVersion);
|
|
|
|
private ExactProjectionWithdrawalOutcome WithdrawCaptured(
|
|
AttachedRemovalCapture captured)
|
|
{
|
|
if (!_attachedByChild.TryGetValue(
|
|
captured.Attached.ChildGuid,
|
|
out AttachedChild? current)
|
|
|| !ReferenceEquals(current, captured.Attached))
|
|
{
|
|
return new ExactProjectionWithdrawalOutcome(
|
|
ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
}
|
|
return WithdrawAttachedProjection(
|
|
captured.Attached.ChildRecord,
|
|
captured.PositionAuthorityVersion,
|
|
captured.ProjectionMutationVersion,
|
|
_withdrawProjection,
|
|
() => CommitProjectionRemoval(captured.Attached));
|
|
}
|
|
|
|
private PendingProjectionSubtree CaptureProjectionSubtreeParentFirst(
|
|
LiveEntityRecord root,
|
|
bool restoreRootRelation,
|
|
bool restoreDescendantRelations)
|
|
{
|
|
var captures = new List<ProjectionRemovalCapture>();
|
|
var visited = new HashSet<LiveEntityRecord>(ReferenceEqualityComparer.Instance);
|
|
CaptureProjectionNode(root, isRoot: true, captures, visited);
|
|
return new PendingProjectionSubtree(
|
|
root,
|
|
root.PositionAuthorityVersion,
|
|
captures,
|
|
NextIndex: 0,
|
|
restoreRootRelation,
|
|
restoreDescendantRelations);
|
|
}
|
|
|
|
private void CaptureProjectionNode(
|
|
LiveEntityRecord record,
|
|
bool isRoot,
|
|
List<ProjectionRemovalCapture> destination,
|
|
HashSet<LiveEntityRecord> visited)
|
|
{
|
|
if (!visited.Add(record))
|
|
return;
|
|
_attachedByChild.TryGetValue(record.ServerGuid, out AttachedChild? attached);
|
|
if (attached is not null && !ReferenceEquals(attached.ChildRecord, record))
|
|
attached = null;
|
|
if (record.IsSpatiallyProjected || attached is not null)
|
|
{
|
|
destination.Add(new ProjectionRemovalCapture(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
record.ProjectionMutationVersion,
|
|
attached,
|
|
isRoot));
|
|
}
|
|
|
|
AttachedChild[] children = _attachedByChild.Values
|
|
.Where(child => ReferenceEquals(child.ParentRecord, record))
|
|
.ToArray();
|
|
for (int i = 0; i < children.Length; i++)
|
|
{
|
|
CaptureProjectionNode(
|
|
children[i].ChildRecord,
|
|
isRoot: false,
|
|
destination,
|
|
visited);
|
|
}
|
|
}
|
|
|
|
private bool BeginProjectionSubtreeWithdrawal(
|
|
Dictionary<uint, PendingProjectionSubtree> pendingByRoot,
|
|
LiveEntityRecord root,
|
|
bool restoreRootRelation,
|
|
bool restoreDescendantRelations)
|
|
{
|
|
PendingProjectionSubtree pending = CaptureProjectionSubtreeParentFirst(
|
|
root,
|
|
restoreRootRelation,
|
|
restoreDescendantRelations);
|
|
return AdvanceProjectionSubtree(pendingByRoot, root.ServerGuid, pending);
|
|
}
|
|
|
|
private bool AdvanceProjectionSubtree(
|
|
Dictionary<uint, PendingProjectionSubtree> pendingByRoot,
|
|
uint rootGuid,
|
|
PendingProjectionSubtree pending)
|
|
{
|
|
ExactProjectionWithdrawalOutcome outcome = AdvanceProjectionSubtree(
|
|
pending,
|
|
out PendingProjectionSubtree next);
|
|
bool retry = outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending
|
|
|| (outcome.Failure is not null && next.NextIndex < next.Captures.Count);
|
|
if (retry)
|
|
pendingByRoot[rootGuid] = next;
|
|
else
|
|
pendingByRoot.Remove(rootGuid);
|
|
if (outcome.Failure is not null)
|
|
throw outcome.Failure;
|
|
return outcome.Disposition is ExactProjectionWithdrawalDisposition.Completed
|
|
&& next.NextIndex >= next.Captures.Count;
|
|
}
|
|
|
|
private ExactProjectionWithdrawalOutcome AdvanceProjectionSubtree(
|
|
PendingProjectionSubtree pending,
|
|
out PendingProjectionSubtree next)
|
|
{
|
|
next = pending;
|
|
if (!_liveEntities.IsCurrentRecord(pending.RootRecord)
|
|
|| pending.RootRecord.PositionAuthorityVersion
|
|
!= pending.RootPositionAuthorityVersion)
|
|
{
|
|
return new(
|
|
ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
}
|
|
|
|
for (int i = pending.NextIndex; i < pending.Captures.Count; i++)
|
|
{
|
|
ProjectionRemovalCapture captured = pending.Captures[i];
|
|
ExactProjectionWithdrawalOutcome outcome = WithdrawProjectionCapture(captured);
|
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending)
|
|
{
|
|
next = pending with { NextIndex = i };
|
|
return outcome;
|
|
}
|
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Superseded
|
|
&& captured.IsRoot)
|
|
{
|
|
next = pending with { NextIndex = i + 1 };
|
|
return outcome;
|
|
}
|
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Completed
|
|
&& captured.Attached is not null
|
|
&& (captured.IsRoot
|
|
? pending.RestoreRootRelation
|
|
: pending.RestoreDescendantRelations))
|
|
{
|
|
Relations.RestoreLastAccepted(captured.Record.ServerGuid);
|
|
}
|
|
next = pending with { NextIndex = i + 1 };
|
|
if (outcome.Failure is not null)
|
|
return outcome;
|
|
}
|
|
|
|
return new(
|
|
ExactProjectionWithdrawalDisposition.Completed,
|
|
Failure: null);
|
|
}
|
|
|
|
private ExactProjectionWithdrawalOutcome WithdrawProjectionCapture(
|
|
ProjectionRemovalCapture captured)
|
|
{
|
|
if (captured.Attached is { } attached)
|
|
{
|
|
return WithdrawCaptured(new AttachedRemovalCapture(
|
|
attached,
|
|
captured.PositionAuthorityVersion,
|
|
captured.ProjectionMutationVersion));
|
|
}
|
|
if (!_liveEntities.IsCurrentRecord(captured.Record)
|
|
|| captured.Record.PositionAuthorityVersion
|
|
!= captured.PositionAuthorityVersion
|
|
|| captured.Record.ProjectionMutationVersion
|
|
!= captured.ProjectionMutationVersion)
|
|
{
|
|
return new(
|
|
ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
}
|
|
if (!captured.Record.IsSpatiallyProjected)
|
|
{
|
|
return new(
|
|
ExactProjectionWithdrawalDisposition.Completed,
|
|
Failure: null);
|
|
}
|
|
return _withdrawProjection(
|
|
captured.Record,
|
|
captured.PositionAuthorityVersion,
|
|
captured.ProjectionMutationVersion);
|
|
}
|
|
|
|
private void RetryProjectionSubtrees(
|
|
Dictionary<uint, PendingProjectionSubtree> pendingByRoot)
|
|
{
|
|
foreach ((uint rootGuid, PendingProjectionSubtree pending) in
|
|
pendingByRoot.ToArray())
|
|
{
|
|
AdvanceProjectionSubtree(pendingByRoot, rootGuid, pending);
|
|
}
|
|
}
|
|
|
|
private static void RemovePendingProjectionSubtree(
|
|
Dictionary<uint, PendingProjectionSubtree> pendingByRoot,
|
|
LiveEntityRecord record)
|
|
{
|
|
uint[] keys = pendingByRoot
|
|
.Where(pair => ReferenceEquals(pair.Value.RootRecord, record))
|
|
.Select(pair => pair.Key)
|
|
.ToArray();
|
|
for (int i = 0; i < keys.Length; i++)
|
|
pendingByRoot.Remove(keys[i]);
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
AttachedChild[] attached = _attachedByChild.Values.ToArray();
|
|
for (int i = 0; i < attached.Length; i++)
|
|
CommitProjectionRemoval(attached[i]);
|
|
_pendingUnparentByChild.Clear();
|
|
_pendingOrdinaryRemovalByRoot.Clear();
|
|
_pendingDetachedRemovalByChild.Clear();
|
|
_pendingReparentRemovalByChild.Clear();
|
|
_pendingPoseLossRemovalByChild.Clear();
|
|
_pendingOrphanRemovalByChild.Clear();
|
|
Relations.Clear();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Clear();
|
|
_objects.ObjectMoved -= OnObjectMoved;
|
|
_objects.MoveRolledBack -= OnMoveRolledBack;
|
|
_objects.ObjectRemovalClassified -= OnObjectRemovalClassified;
|
|
}
|
|
|
|
private sealed record AttachedChild(
|
|
LiveEntityRecord ParentRecord,
|
|
LiveEntityRecord ChildRecord,
|
|
uint ParentGuid,
|
|
uint ChildGuid,
|
|
ParentLocation ParentLocation,
|
|
Placement Placement,
|
|
Setup ParentSetup,
|
|
Setup ChildSetup,
|
|
IReadOnlyList<MeshRef> PartTemplate,
|
|
IReadOnlyList<bool> PartAvailability,
|
|
Matrix4x4[] PartPoseBuffer,
|
|
MeshRef[] AttachedPartBuffer,
|
|
float Scale,
|
|
WorldEntity Entity);
|
|
|
|
private readonly record struct PendingUnparentTransition(
|
|
LiveEntityRecord Record,
|
|
ulong PositionAuthorityVersion,
|
|
PendingProjectionSubtree Subtree,
|
|
Action? Continuation);
|
|
|
|
private readonly record struct PendingProjectionSubtree(
|
|
LiveEntityRecord RootRecord,
|
|
ulong RootPositionAuthorityVersion,
|
|
IReadOnlyList<ProjectionRemovalCapture> Captures,
|
|
int NextIndex,
|
|
bool RestoreRootRelation,
|
|
bool RestoreDescendantRelations);
|
|
|
|
private readonly record struct ProjectionRemovalCapture(
|
|
LiveEntityRecord Record,
|
|
ulong PositionAuthorityVersion,
|
|
ulong ProjectionMutationVersion,
|
|
AttachedChild? Attached,
|
|
bool IsRoot);
|
|
|
|
private readonly record struct AttachedRemovalCapture(
|
|
AttachedChild Attached,
|
|
ulong PositionAuthorityVersion,
|
|
ulong ProjectionMutationVersion);
|
|
|
|
private readonly record struct PendingOrdinaryRemoval(
|
|
LiveEntityRecord RootRecord,
|
|
IReadOnlyList<AttachedRemovalCapture> Captures,
|
|
int NextIndex);
|
|
}
|
|
|
|
public enum ChildUnparentDisposition
|
|
{
|
|
NotAttached,
|
|
Completed,
|
|
Pending,
|
|
Superseded,
|
|
}
|
|
|
|
internal enum ParentProjectionValidationDisposition
|
|
{
|
|
Ready,
|
|
Waiting,
|
|
Rejected,
|
|
}
|