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

@ -1,16 +1,48 @@
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
namespace AcDream.App.World;
internal sealed class DelegateLiveEntitySpawnRelationshipSink(
Action<WorldSession.EntitySpawn> onSpawn) : ILiveEntitySpawnRelationshipSink
internal sealed class LiveEntityRelationshipProjection(
EquippedChildRenderController children) : ILiveEntityRelationshipProjection
{
private readonly Action<WorldSession.EntitySpawn> _onSpawn =
onSpawn ?? throw new ArgumentNullException(nameof(onSpawn));
private readonly EquippedChildRenderController _children =
children ?? throw new ArgumentNullException(nameof(children));
public void OnSpawn(WorldSession.EntitySpawn spawn) => _onSpawn(spawn);
public void OnSpawn(WorldSession.EntitySpawn spawn) => _children.OnSpawn(spawn);
public void OnParent(ParentEvent.Parsed update) => _children.OnParentEvent(update);
public void OnCreateParentAccepted(CreateParentUpdate update) =>
_children.OnCreateParentAccepted(update);
public ChildUnparentDisposition OnChildBecameUnparented(uint childGuid) =>
_children.OnChildBecameUnparented(childGuid);
public bool TryApplyAttachedAppearance(
LiveEntityRecord record,
ulong objDescAuthorityVersion) =>
_children.TryApplyAttachedAppearance(record, objDescAuthorityVersion);
}
/// <summary>
/// Fail-fast construction seam for the retail ParentEvent timestamp gate.
/// The equipped-child renderer can be built before hydration, but no live
/// session may resolve a relation until hydration binds this once.
/// </summary>
internal sealed class DeferredLiveEntityParentAcceptance
{
private Func<ParentEvent.Parsed, bool>? _accept;
public void Bind(Func<ParentEvent.Parsed, bool> accept)
{
ArgumentNullException.ThrowIfNull(accept);
if (Interlocked.CompareExchange(ref _accept, accept, null) is not null)
throw new InvalidOperationException("Live parent acceptance is already bound.");
}
public bool TryAccept(ParentEvent.Parsed update) =>
(_accept ?? throw new InvalidOperationException(
"Live parent acceptance must be bound before a session starts."))(update);
}
internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
@ -49,31 +81,23 @@ internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
?? throw new ArgumentNullException(nameof(replayEffects));
}
public bool Publish(LiveEntityRecord expectedRecord)
public bool Publish(LiveEntityReadyCandidate candidate)
{
LiveEntityRecord expectedRecord = candidate.Record;
ArgumentNullException.ThrowIfNull(expectedRecord);
ulong createIntegrationVersion = expectedRecord.CreateIntegrationVersion;
if (!_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion)
if (!candidate.IsCurrent(_runtime)
|| !_prepareEffects(expectedRecord)
|| !_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion))
|| !candidate.IsCurrent(_runtime))
return false;
if (!_presentState(expectedRecord)
|| !_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion))
|| !candidate.IsCurrent(_runtime))
{
return false;
}
return _replayEffects(expectedRecord)
&& _runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion);
&& candidate.IsCurrent(_runtime);
}
}