feat(vfx): port retail hidden and teleport presentation

Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
This commit is contained in:
Erik 2026-07-14 14:59:48 +02:00
parent a51ebc66e9
commit 1e98d81448
46 changed files with 4883 additions and 127 deletions

View file

@ -219,6 +219,7 @@ public sealed class EquippedChildRenderController : IDisposable
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 (parent.ParentCellId is { } parentCellId)
@ -317,6 +318,7 @@ public sealed class EquippedChildRenderController : IDisposable
if (entity is null)
return false;
ApplyParentWorldPose(entity, parentWorld);
ApplyParentDrawVisibility(entity, parentEntity);
entity.ParentCellId = parentCellId;
entity.ApplyAppearance(
pose.AttachedParts,
@ -338,6 +340,14 @@ public sealed class EquippedChildRenderController : IDisposable
pose.AttachedParts,
scale,
entity);
if (_liveEntities.TryGetRecord(pending.ParentGuid, out LiveEntityRecord parentRecord)
&& (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} " +
@ -376,6 +386,18 @@ public sealed class EquippedChildRenderController : IDisposable
return true;
}
/// <summary>
/// Preserve retail attachment-tree draw inheritance after adapting every
/// child CPhysicsObj into an independent retained draw entry. This does not
/// alter the child's own NoDraw state; parent-first ticking propagates an
/// ancestor's suppression through arbitrarily deep attachment chains.
/// </summary>
internal static void ApplyParentDrawVisibility(WorldEntity child, WorldEntity parent)
{
child.IsAncestorDrawVisible =
parent.IsDrawVisible && parent.IsAncestorDrawVisible;
}
private static bool TryDecomposeWorldPose(
Matrix4x4 world,
out Vector3 position,
@ -425,6 +447,20 @@ public sealed class EquippedChildRenderController : IDisposable
return null;
}
/// <summary>
/// Retail <c>CPhysicsObj::set_hidden</c> walks the direct CHILDLIST and
/// sets/clears each child's NoDraw bit before changing root collision/cell
/// presentation. Descendants are not recursively rewritten here.
/// </summary>
public void SetDirectChildrenNoDraw(uint parentGuid, bool noDraw)
{
foreach (AttachedChild child in _attachedByChild.Values)
{
if (child.ParentGuid == parentGuid)
_liveEntities.SetAttachedChildNoDraw(child.ChildGuid, noDraw);
}
}
private void ResolveRelations(uint childGuid)
{
Relations.Resolve(