refactor(world): extract live appearance and parenting

Move ObjDesc, Parent, Pickup, child-ready, and retained projection recovery into LiveEntityHydrationController while preserving retail timestamp and leave-world semantics. Pin ready publication to exact projection authority and restore attached descendant chains synchronously.
This commit is contained in:
Erik 2026-07-21 18:10:07 +02:00
parent 40352d5a7a
commit fe5514967c
9 changed files with 1098 additions and 199 deletions

View file

@ -98,9 +98,12 @@ public sealed class EquippedChildRenderController : IDisposable
lock (_datLock)
{
if (spawn.ParentGuid is { } parentGuid and not 0
&& spawn.ParentLocation is { } parentLocation
&& spawn.PlacementId is { } placementId)
&& 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,
@ -118,6 +121,51 @@ public sealed class EquippedChildRenderController : IDisposable
}
}
/// <summary>
/// Retail <c>SmartBox::UpdateVisualDesc @ 0x00451F40</c> replaces an
/// attached child's visual description without requiring a world
/// Position. Re-realize the last accepted relation from the newest
/// canonical snapshot while retaining the logical entity.
/// </summary>
internal bool TryApplyAttachedAppearance(
LiveEntityRecord expectedRecord,
ulong expectedObjDescAuthorityVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
lock (_datLock)
{
if (!_liveEntities.IsCurrentObjDescAuthority(
expectedRecord,
expectedObjDescAuthorityVersion)
|| expectedRecord.ProjectionKind is not
LiveEntityProjectionKind.Attached
|| !expectedRecord.IsSpatiallyProjected
|| expectedRecord.WorldEntity is not { } expectedEntity
|| !Relations.RestoreLastAccepted(expectedRecord.ServerGuid))
{
return false;
}
bool projected = ResolveAndTryRealize(expectedRecord.ServerGuid);
if (projected)
{
// WithdrawPriorProjection removes the complete attached
// subtree and retains each descendant relation for recovery.
// The ObjDesc transaction is not published until that same
// subtree is synchronously whole again.
RetryWaitingDescendants(expectedRecord.ServerGuid);
}
return projected
&& _liveEntities.IsCurrentObjDescAuthority(
expectedRecord,
expectedObjDescAuthorityVersion)
&& expectedRecord.ProjectionKind is
LiveEntityProjectionKind.Attached
&& expectedRecord.IsSpatiallyProjected
&& ReferenceEquals(expectedRecord.WorldEntity, expectedEntity);
}
}
/// <summary>
/// Completes attachment projection after a root world entity has been
/// registered. Relation acceptance intentionally happens before root
@ -448,46 +496,31 @@ public sealed class EquippedChildRenderController : IDisposable
// CPhysicsObj::set_hidden (0x00514C60).
_liveEntities.SetAttachedChildNoDraw(childGuid, noDraw: true);
}
ulong readyCreateIntegrationVersion = childRecord.CreateIntegrationVersion;
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,
childRecord,
readyCreateIntegrationVersion,
entity,
readyCandidate,
EntityReady);
}
internal static bool PublishEntityReadyExact(
LiveEntityRuntime runtime,
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion,
WorldEntity expectedEntity,
LiveEntityReadyCandidate candidate,
Action<LiveEntityReadyCandidate>? publish)
{
ArgumentNullException.ThrowIfNull(runtime);
ArgumentNullException.ThrowIfNull(expectedRecord);
ArgumentNullException.ThrowIfNull(expectedEntity);
if (!runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion)
|| !ReferenceEquals(expectedRecord.WorldEntity, expectedEntity))
{
if (!candidate.IsCurrent(runtime))
return false;
}
publish?.Invoke(new LiveEntityReadyCandidate(
expectedRecord,
expectedCreateIntegrationVersion));
return runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion)
&& ReferenceEquals(expectedRecord.WorldEntity, expectedEntity);
publish?.Invoke(candidate);
return candidate.IsCurrent(runtime);
}
private void PublishChildPose(