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, }