feat(vfx): bind effects to live animated poses
This commit is contained in:
parent
96ddfdf175
commit
542dcfc384
41 changed files with 3246 additions and 741 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.World;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Net;
|
||||
|
|
@ -27,12 +28,20 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
private readonly ClientObjectTable _objects;
|
||||
private readonly LiveEntityRuntime _liveEntities;
|
||||
private readonly Func<ParentEvent.Parsed, bool> _acceptParent;
|
||||
private readonly EntityEffectPoseRegistry _poses;
|
||||
|
||||
private ParentAttachmentState Relations => _liveEntities.ParentAttachments;
|
||||
|
||||
/// <summary>Raised after the attached projection is fully registered.</summary>
|
||||
public event Action<uint>? 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 AttachmentUpdateOrder<AttachedChild> _updateOrder = new();
|
||||
private readonly Func<AttachedChild, uint?> _parentOfAttached;
|
||||
private readonly Func<uint, bool> _tickAttached;
|
||||
|
||||
public IEnumerable<uint> AttachedEntityIds
|
||||
{
|
||||
|
|
@ -48,13 +57,17 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
object datLock,
|
||||
ClientObjectTable objects,
|
||||
LiveEntityRuntime liveEntities,
|
||||
EntityEffectPoseRegistry poses,
|
||||
Func<ParentEvent.Parsed, bool> acceptParent)
|
||||
{
|
||||
_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));
|
||||
_parentOfAttached = static child => child.ParentGuid;
|
||||
_tickAttached = TickChild;
|
||||
|
||||
_objects.ObjectMoved += OnObjectMoved;
|
||||
_objects.MoveRolledBack += OnMoveRolledBack;
|
||||
|
|
@ -83,17 +96,11 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
spawn.PositionSequence));
|
||||
}
|
||||
|
||||
ResolveRelations(spawn.Guid);
|
||||
TryRealize(spawn.Guid);
|
||||
ResolveAndTryRealize(spawn.Guid);
|
||||
|
||||
// ParentEvent can precede the parent's CreateObject. Revisit every
|
||||
// child waiting specifically on the object that just arrived.
|
||||
IReadOnlyList<uint> waiting = Relations.ChildrenWaitingForParent(spawn.Guid);
|
||||
for (int i = 0; i < waiting.Count; i++)
|
||||
{
|
||||
ResolveRelations(waiting[i]);
|
||||
TryRealize(waiting[i]);
|
||||
}
|
||||
// ParentEvent can precede the parent's CreateObject. Revisit the
|
||||
// complete descendant chain rooted at the object that arrived.
|
||||
RetryWaitingDescendants(spawn.Guid);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,15 +114,20 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
{
|
||||
lock (_datLock)
|
||||
{
|
||||
IReadOnlyList<uint> waiting = Relations.ChildrenWaitingForParent(guid);
|
||||
for (int i = 0; i < waiting.Count; i++)
|
||||
{
|
||||
ResolveRelations(waiting[i]);
|
||||
TryRealize(waiting[i]);
|
||||
}
|
||||
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
|
||||
|
|
@ -126,8 +138,8 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
lock (_datLock)
|
||||
{
|
||||
Relations.Enqueue(update);
|
||||
ResolveRelations(update.ChildGuid);
|
||||
TryRealize(update.ChildGuid);
|
||||
if (ResolveAndTryRealize(update.ChildGuid))
|
||||
RetryWaitingDescendants(update.ChildGuid);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,68 +183,103 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
/// <summary>Recompose every child after the parent's animation tick.</summary>
|
||||
public void Tick()
|
||||
{
|
||||
foreach (AttachedChild child in _attachedByChild.Values)
|
||||
{
|
||||
if (!_liveEntities.TryGetWorldEntity(child.ParentGuid, out WorldEntity parent))
|
||||
continue;
|
||||
|
||||
if (!EquippedChildAttachment.TryCompose(
|
||||
child.ParentSetup,
|
||||
parent.MeshRefs,
|
||||
child.ChildSetup,
|
||||
child.ParentLocation,
|
||||
child.Placement,
|
||||
child.PartTemplate,
|
||||
child.Scale,
|
||||
out IReadOnlyList<MeshRef> parts))
|
||||
continue;
|
||||
|
||||
child.Entity.MeshRefs = parts;
|
||||
child.Entity.SetPosition(parent.Position);
|
||||
child.Entity.Rotation = parent.Rotation;
|
||||
child.Entity.ParentCellId = parent.ParentCellId;
|
||||
if (parent.ParentCellId is { } parentCellId)
|
||||
_liveEntities.RebucketLiveEntity(child.ChildGuid, parentCellId);
|
||||
}
|
||||
IReadOnlyList<uint> failed = _updateOrder.ForEachParentFirst(
|
||||
_attachedByChild,
|
||||
_parentOfAttached,
|
||||
_tickAttached);
|
||||
for (int i = 0; i < failed.Count; i++)
|
||||
WithdrawForPoseLoss(failed[i]);
|
||||
}
|
||||
|
||||
private void TryRealize(uint childGuid)
|
||||
private bool TickChild(uint childGuid)
|
||||
{
|
||||
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child))
|
||||
return false;
|
||||
|
||||
if (_liveEntities.TryGetWorldEntity(child.ParentGuid, 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;
|
||||
child.Entity.ParentCellId = parent.ParentCellId;
|
||||
PublishChildPose(child.Entity, parentWorld, parent.ParentCellId, pose);
|
||||
if (parent.ParentCellId is { } parentCellId)
|
||||
_liveEntities.RebucketLiveEntity(child.ChildGuid, parentCellId);
|
||||
ProjectionPoseReady?.Invoke(child.ChildGuid);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryRealize(uint childGuid)
|
||||
{
|
||||
if (!Relations.TryGetProjection(childGuid, out ParentAttachmentRelation pending))
|
||||
return;
|
||||
return false;
|
||||
|
||||
if (!_liveEntities.TryGetWorldEntity(pending.ParentGuid, out WorldEntity parentEntity)
|
||||
|| !_liveEntities.TryGetSnapshot(pending.ParentGuid, out WorldSession.EntitySpawn parentSpawn)
|
||||
|| !_liveEntities.TryGetSnapshot(childGuid, out WorldSession.EntitySpawn childSpawn))
|
||||
return;
|
||||
return false;
|
||||
|
||||
if (parentSpawn.SetupTableId is not { } parentSetupId
|
||||
|| childSpawn.SetupTableId is not { } childSetupId
|
||||
|| parentEntity.ParentCellId is not { } parentCellId)
|
||||
return;
|
||||
return false;
|
||||
|
||||
Setup? parentSetup = _dats.Get<Setup>(parentSetupId);
|
||||
Setup? childSetup = _dats.Get<Setup>(childSetupId);
|
||||
if (parentSetup is null || childSetup is null)
|
||||
return;
|
||||
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.TryCompose(
|
||||
if (!EquippedChildAttachment.TryComposePoseInto(
|
||||
parentSetup,
|
||||
parentEntity.MeshRefs,
|
||||
parentPartPoses,
|
||||
parentPartAvailability,
|
||||
childSetup,
|
||||
parentLocation,
|
||||
placement,
|
||||
template,
|
||||
scale,
|
||||
out IReadOnlyList<MeshRef> parts))
|
||||
return;
|
||||
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
|
||||
|
|
@ -250,29 +297,34 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
WorldEntity? entity = _liveEntities.MaterializeLiveEntity(
|
||||
childGuid,
|
||||
parentCellId,
|
||||
localId => new WorldEntity
|
||||
localId =>
|
||||
{
|
||||
Id = localId,
|
||||
ServerGuid = childGuid,
|
||||
SourceGfxObjOrSetupId = childSetupId,
|
||||
Position = parentEntity.Position,
|
||||
Rotation = parentEntity.Rotation,
|
||||
MeshRefs = parts,
|
||||
PaletteOverride = BuildPaletteOverride(childSpawn),
|
||||
ParentCellId = parentCellId,
|
||||
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;
|
||||
entity.SetPosition(parentEntity.Position);
|
||||
entity.Rotation = parentEntity.Rotation;
|
||||
return false;
|
||||
ApplyParentWorldPose(entity, parentWorld);
|
||||
entity.ParentCellId = parentCellId;
|
||||
entity.ApplyAppearance(
|
||||
parts,
|
||||
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(
|
||||
pending.ParentGuid,
|
||||
childGuid,
|
||||
|
|
@ -281,13 +333,60 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
parentSetup,
|
||||
childSetup,
|
||||
template,
|
||||
childPartAvailability,
|
||||
pose.PartLocal,
|
||||
pose.AttachedParts,
|
||||
scale,
|
||||
entity);
|
||||
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(childGuid);
|
||||
ProjectionPoseReady?.Invoke(childGuid);
|
||||
EntityReady?.Invoke(childGuid);
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
|
|
@ -337,6 +436,20 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
_acceptParent);
|
||||
}
|
||||
|
||||
private bool ResolveAndTryRealize(uint childGuid)
|
||||
{
|
||||
ResolveRelations(childGuid);
|
||||
return TryRealize(childGuid);
|
||||
}
|
||||
|
||||
private void RetryWaitingDescendants(uint parentGuid)
|
||||
{
|
||||
_updateOrder.RealizeDescendants(
|
||||
parentGuid,
|
||||
Relations.ChildrenWaitingForParent,
|
||||
ResolveAndTryRealize);
|
||||
}
|
||||
|
||||
private IReadOnlyList<MeshRef> BuildPartTemplate(
|
||||
Setup setup,
|
||||
WorldSession.EntitySpawn spawn)
|
||||
|
|
@ -396,6 +509,14 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
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)
|
||||
|
|
@ -430,27 +551,52 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
if (Relations.RestoreLastAccepted(item.ObjectId))
|
||||
{
|
||||
lock (_datLock)
|
||||
TryRealize(item.ObjectId);
|
||||
{
|
||||
if (ResolveAndTryRealize(item.ObjectId))
|
||||
RetryWaitingDescendants(item.ObjectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Remove(uint childGuid)
|
||||
{
|
||||
if (!_attachedByChild.Remove(childGuid))
|
||||
if (!_attachedByChild.Remove(childGuid, out AttachedChild? child))
|
||||
return;
|
||||
_liveEntities.WithdrawLiveEntityProjection(childGuid);
|
||||
_poses.Remove(child.Entity.Id);
|
||||
ProjectionRemoved?.Invoke(child.Entity.Id);
|
||||
}
|
||||
|
||||
private void WithdrawForPoseLoss(uint childGuid)
|
||||
{
|
||||
if (!_attachedByChild.ContainsKey(childGuid))
|
||||
return;
|
||||
Remove(childGuid);
|
||||
// MarkProjected removed the active candidate but retained the last
|
||||
// accepted relationship. Requeue that exact accepted relation so a
|
||||
// later parent pose/appearance registration can realize it again.
|
||||
Relations.RestoreLastAccepted(childGuid);
|
||||
}
|
||||
|
||||
private void TearDownObjectProjections(uint guid)
|
||||
{
|
||||
Remove(guid);
|
||||
|
||||
uint[] children = _attachedByChild.Values
|
||||
.Where(child => child.ParentGuid == guid)
|
||||
.Select(child => child.ChildGuid)
|
||||
.ToArray();
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
Remove(children[i]);
|
||||
IReadOnlyList<uint> subtree = _updateOrder.CollectSubtreePostOrder(
|
||||
_attachedByChild,
|
||||
guid,
|
||||
_parentOfAttached);
|
||||
for (int i = 0; i < subtree.Count; i++)
|
||||
{
|
||||
uint childGuid = subtree[i];
|
||||
Remove(childGuid);
|
||||
if (childGuid != guid)
|
||||
{
|
||||
// Retail unparents direct children. Descendant relationships
|
||||
// can become renderable again if their immediate parent later
|
||||
// acquires a top-level Position, so retain their accepted
|
||||
// relationship as a pending projection candidate.
|
||||
Relations.RestoreLastAccepted(childGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
|
|
@ -477,6 +623,9 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
Setup ParentSetup,
|
||||
Setup ChildSetup,
|
||||
IReadOnlyList<MeshRef> PartTemplate,
|
||||
IReadOnlyList<bool> PartAvailability,
|
||||
Matrix4x4[] PartPoseBuffer,
|
||||
MeshRef[] AttachedPartBuffer,
|
||||
float Scale,
|
||||
WorldEntity Entity);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue