refactor(world): extract live entity hydration

This commit is contained in:
Erik 2026-07-21 17:32:45 +02:00
parent 27dcd0ecb7
commit d10c5f2d54
17 changed files with 4310 additions and 1269 deletions

File diff suppressed because it is too large Load diff

View file

@ -36,7 +36,7 @@ public sealed class EquippedChildRenderController : IDisposable
private ParentAttachmentState Relations => _liveEntities.ParentAttachments;
/// <summary>Raised after the attached projection is fully registered.</summary>
public event Action<uint>? EntityReady;
internal event Action<LiveEntityReadyCandidate>? 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>
@ -448,14 +448,46 @@ 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);
ProjectionPoseReady?.Invoke(childGuid);
EntityReady?.Invoke(childGuid);
return true;
return PublishEntityReadyExact(
_liveEntities,
childRecord,
readyCreateIntegrationVersion,
entity,
EntityReady);
}
internal static bool PublishEntityReadyExact(
LiveEntityRuntime runtime,
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion,
WorldEntity expectedEntity,
Action<LiveEntityReadyCandidate>? publish)
{
ArgumentNullException.ThrowIfNull(runtime);
ArgumentNullException.ThrowIfNull(expectedRecord);
ArgumentNullException.ThrowIfNull(expectedEntity);
if (!runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion)
|| !ReferenceEquals(expectedRecord.WorldEntity, expectedEntity))
{
return false;
}
publish?.Invoke(new LiveEntityReadyCandidate(
expectedRecord,
expectedCreateIntegrationVersion));
return runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion)
&& ReferenceEquals(expectedRecord.WorldEntity, expectedEntity);
}
private void PublishChildPose(

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,105 @@
using AcDream.App.World;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Rendering;
/// <summary>
/// Completes a fresher same-incarnation CreateObject after it interrupted an
/// active projection transaction. Logical entity/resource ownership is
/// retained; the exact current appearance is republished, the plugin/current
/// snapshot is refreshed, and the retained animation owner is synchronized in
/// that order. Every potentially reentrant stage is guarded by the monotonic
/// Create-integration authority.
/// </summary>
internal static class LiveEntityCreateSupersessionRecovery
{
public static bool TryApply(
LiveEntityRuntime runtime,
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion,
Func<LiveEntityAppearanceUpdateState?> captureAppearance,
Func<LiveEntityAppearanceUpdateState, bool> publishAppearance,
Action<LiveEntityAppearanceUpdateState> publishCurrentSnapshot,
Action<LiveEntityAppearanceUpdateState> synchronizeAnimation)
{
ArgumentNullException.ThrowIfNull(runtime);
ArgumentNullException.ThrowIfNull(expectedRecord);
ArgumentNullException.ThrowIfNull(captureAppearance);
ArgumentNullException.ThrowIfNull(publishAppearance);
ArgumentNullException.ThrowIfNull(publishCurrentSnapshot);
ArgumentNullException.ThrowIfNull(synchronizeAnimation);
if (!runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion)
|| captureAppearance() is not { } visualUpdate
|| !runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion)
|| !publishAppearance(visualUpdate)
|| !runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion))
{
return false;
}
publishCurrentSnapshot(visualUpdate);
if (!runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion))
{
return false;
}
synchronizeAnimation(visualUpdate);
return runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion);
}
}
/// <summary>
/// Reconciles the exceptional case where a fresher same-incarnation Create
/// interrupts initial PartArray construction. Completed owners already route
/// MovementData through SetObjectMovement and therefore retain their exact
/// sequencer, dispatch sink, and paired manager/interpreter queues.
/// </summary>
internal static class LiveEntityCreateAnimationSynchronization
{
public static bool TrySynchronizeInterruptedInitialOwner(
LiveEntityRecord record,
LiveEntityAnimationState animation,
Animation? canonicalAnimation,
int canonicalLowFrame,
int canonicalHighFrame,
float canonicalFramerate,
MotionTable? motionTable,
CreateObject.ServerMotionState? wireState)
{
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(animation);
if (record.InitialHydrationCompleted)
return false;
if (canonicalAnimation is not null)
{
animation.Animation = canonicalAnimation;
animation.LowFrame = canonicalLowFrame;
animation.HighFrame = canonicalHighFrame;
animation.Framerate = canonicalFramerate;
animation.CurrFrame = canonicalLowFrame;
}
if (animation.Sequencer is { } sequencer && motionTable is not null)
{
SpawnMotionInitializer.Reinitialize(
sequencer,
motionTable,
wireState);
}
return true;
}
}

View file

@ -41,6 +41,26 @@ internal static class SpawnMotionInitializer
return sequencer;
}
/// <summary>
/// Replays description-time MovementData into the same retained PartArray
/// sequencer. This is used only when a fresher same-incarnation CreateObject
/// interrupted initial hydration before the object crossed its ready edge;
/// completed objects consume the packet through SetObjectMovement instead.
/// </summary>
public static void Reinitialize(
AnimationSequencer sequencer,
MotionTable motionTable,
CreateObject.ServerMotionState? wireState)
{
ArgumentNullException.ThrowIfNull(sequencer);
ArgumentNullException.ThrowIfNull(motionTable);
Plan plan = ResolvePlan(motionTable, wireState);
sequencer.Reset();
sequencer.InitializeState();
sequencer.SetCycle(plan.Style, plan.Motion);
sequencer.Manager.HandleEnterWorld();
}
internal static Plan ResolvePlan(
MotionTable motionTable,
CreateObject.ServerMotionState? wireState)