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;
///
/// 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
/// and recomposes it after the parent's animation
/// advances each frame.
///
public sealed class EquippedChildRenderController : IDisposable
{
private readonly IDatReaderWriter _dats;
private readonly object _datLock;
private readonly ClientObjectTable _objects;
private readonly LiveEntityRuntime _liveEntities;
private readonly Func _acceptParent;
private readonly Func
_withdrawProjection;
private readonly EntityEffectPoseRegistry _poses;
private ParentAttachmentState Relations => _liveEntities.ParentAttachments;
/// Raised after the attached projection is fully registered.
internal event Action? EntityReady;
/// Raised after an attached projection has its composed pose.
public event Action? ProjectionPoseReady;
/// Raised when an attached projection leaves its cell presentation.
public event Action? ProjectionRemoved;
private readonly Dictionary _attachedByChild = new();
private readonly Dictionary _pendingUnparentByChild = new();
private readonly Dictionary _pendingOrdinaryRemovalByRoot = new();
private readonly Dictionary _pendingDetachedRemovalByChild = new();
private readonly Dictionary _pendingReparentRemovalByChild = new();
private readonly Dictionary _pendingPoseLossRemovalByChild = new();
private readonly Dictionary _pendingOrphanRemovalByChild = new();
private readonly List _pendingProjectionChildrenScratch = new();
private readonly AttachmentUpdateOrder _updateOrder = new();
private readonly Func _parentOfAttached;
private readonly Func _tickAttached;
public IEnumerable 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 acceptParent,
Func
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;
}
///
/// 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.
///
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);
}
}
///
/// Retail SmartBox::UpdateVisualDesc @ 0x00451F40 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.
///
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);
}
}
///
/// 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.
///
public void OnWorldEntityRegistered(uint guid)
{
lock (_datLock)
{
RetryWaitingDescendants(guid);
}
}
///
/// Retries pose-withdrawn descendants only after their parent's new root
/// and indexed parts have been published.
///
public void OnPosePublished(uint guid)
{
lock (_datLock)
RetryWaitingDescendants(guid);
}
///
/// 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.
///
public void OnParentEvent(ParentEvent.Parsed update)
{
lock (_datLock)
{
Relations.Enqueue(update);
if (ResolveAndTryRealize(update.ChildGuid))
RetryWaitingDescendants(update.ChildGuid);
}
}
///
/// 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.
///
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.
}
///
/// 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.
///
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);
}
/// Recompose every child after the parent's animation tick.
public void Tick()
{
RetryPendingProjectionTransitions();
IReadOnlyList 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(parentSetupId);
Setup? childSetup = _dats.Get(childSetupId);
if (parentSetup is null || childSetup is null)
return false;
var parentLocation = (ParentLocation)pending.ParentLocation;
var placement = (Placement)pending.PlacementId;
IReadOnlyList 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());
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? 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);
}
///
/// 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.
///
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;
}
///
/// 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.
///
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;
}
///
/// Resolves retail's parent Setup part index to the currently attached
/// child local ID for DefaultScriptPartHook.
///
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;
}
/// Returns the live parent whose UpdateChild path owns this child.
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;
}
///
/// Retail CPhysicsObj::set_hidden walks the direct CHILDLIST and
/// sets/clears each child's NoDraw bit before changing root collision/cell
/// presentation. Descendants are not recursively rewritten here.
///
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(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 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 partChanges =
spawn.AnimPartChanges ?? Array.Empty();
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 textureChanges =
spawn.TextureChanges ?? Array.Empty();
for (int partIndex = 0; partIndex < result.Length; partIndex++)
{
Dictionary? oldToNew = null;
for (int t = 0; t < textureChanges.Count; t++)
{
CreateObject.TextureChange change = textureChanges[t];
if (change.PartIndex != partIndex) continue;
oldToNew ??= new Dictionary();
oldToNew[change.OldTexture] = change.NewTexture;
}
if (oldToNew is null) continue;
GfxObj? gfx = _dats.Get(result[partIndex].GfxObjId);
if (gfx is null) continue;
Dictionary? surfaceOverrides = null;
foreach (var surfaceQid in gfx.Surfaces)
{
uint surfaceId = (uint)surfaceQid;
Surface? surface = _dats.Get(surfaceId);
if (surface is null) continue;
uint oldTexture = (uint)surface.OrigTextureId;
if (!oldToNew.TryGetValue(oldTexture, out uint replacement)) continue;
surfaceOverrides ??= new Dictionary();
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 template)
{
var available = new bool[template.Count];
for (int i = 0; i < template.Count; i++)
available[i] = _dats.Get(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
withdrawProjection,
Func 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 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 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 CaptureRecordSubtreeParentFirst(
LiveEntityRecord record)
{
var result = new List();
var visited = new HashSet(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 destination,
HashSet 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();
var visited = new HashSet(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 destination,
HashSet 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 pendingByRoot,
LiveEntityRecord root,
bool restoreRootRelation,
bool restoreDescendantRelations)
{
PendingProjectionSubtree pending = CaptureProjectionSubtreeParentFirst(
root,
restoreRootRelation,
restoreDescendantRelations);
return AdvanceProjectionSubtree(pendingByRoot, root.ServerGuid, pending);
}
private bool AdvanceProjectionSubtree(
Dictionary 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 pendingByRoot)
{
foreach ((uint rootGuid, PendingProjectionSubtree pending) in
pendingByRoot.ToArray())
{
AdvanceProjectionSubtree(pendingByRoot, rootGuid, pending);
}
}
private static void RemovePendingProjectionSubtree(
Dictionary 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 PartTemplate,
IReadOnlyList 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 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 Captures,
int NextIndex);
}
public enum ChildUnparentDisposition
{
NotAttached,
Completed,
Pending,
Superseded,
}
internal enum ParentProjectionValidationDisposition
{
Ready,
Waiting,
Rejected,
}