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)

View file

@ -40,6 +40,7 @@ public sealed record RuntimeOptions(
bool RetailCloseDegrades,
bool DumpSceneryZ,
bool DumpLiveSpawns,
bool DumpClothing,
int? LegacyStreamRadius,
bool RetailUi,
string? AcDir,
@ -89,6 +90,7 @@ public sealed record RuntimeOptions(
RetailCloseDegrades: !string.Equals(env("ACDREAM_RETAIL_CLOSE_DEGRADES"), "0", StringComparison.Ordinal),
DumpSceneryZ: IsExactlyOne(env("ACDREAM_DUMP_SCENERY_Z")),
DumpLiveSpawns: IsExactlyOne(env("ACDREAM_DUMP_LIVE_SPAWNS")),
DumpClothing: IsExactlyOne(env("ACDREAM_DUMP_CLOTHING")),
// Legacy override for ACDREAM_STREAM_RADIUS. Caller applies it on
// top of the quality preset's radii. Null when unset or invalid.
LegacyStreamRadius: TryParseNonNegativeInt(env("ACDREAM_STREAM_RADIUS")),

View file

@ -0,0 +1,613 @@
using AcDream.App.Rendering;
using AcDream.Core.Items;
using AcDream.Core.Net;
namespace AcDream.App.World;
internal enum LiveProjectionPurpose
{
LogicalRegistration,
SpatialRecovery,
CreateSupersessionRecovery,
AppearanceMutation,
}
/// <summary>
/// DAT-backed projection boundary used by <see cref="LiveEntityHydrationController"/>.
/// The implementation owns mesh/animation/collision construction, but never
/// owns or indexes live identity.
/// </summary>
internal interface ILiveEntityProjectionMaterializer
{
bool TryMaterialize(
LiveEntityRecord expectedRecord,
WorldSession.EntitySpawn canonicalSpawn,
LiveProjectionPurpose purpose,
ulong expectedCreateIntegrationVersion,
LiveEntityAppearanceUpdateState? appearanceUpdate = null);
void ResetSessionState();
}
internal interface ILiveEntitySpawnRelationshipSink
{
void OnSpawn(WorldSession.EntitySpawn spawn);
}
internal interface ILiveEntityReadyPublisher
{
bool Publish(LiveEntityRecord expectedRecord);
}
internal readonly record struct LiveEntityReadyCandidate(
LiveEntityRecord Record,
ulong CreateIntegrationVersion);
internal interface ILiveEntityNetworkUpdateSink
{
void ApplySameGeneration(SameGenerationCreateObjectEvents events);
}
/// <summary>
/// Single-assignment construction seam between hydration and the Slice-4F
/// network-update owner. A session cannot publish CreateObject until this is
/// bound.
/// </summary>
internal sealed class DeferredLiveEntityNetworkUpdateSink : ILiveEntityNetworkUpdateSink
{
private ILiveEntityNetworkUpdateSink? _inner;
public void Bind(ILiveEntityNetworkUpdateSink inner)
{
ArgumentNullException.ThrowIfNull(inner);
if (Interlocked.CompareExchange(ref _inner, inner, null) is not null)
throw new InvalidOperationException("The live-entity network sink is already bound.");
}
public void ApplySameGeneration(SameGenerationCreateObjectEvents events) =>
(_inner ?? throw new InvalidOperationException(
"The live-entity network sink must be bound before a session starts."))
.ApplySameGeneration(events);
}
internal sealed class DelegateLiveEntityNetworkUpdateSink(
Action<SameGenerationCreateObjectEvents> apply) : ILiveEntityNetworkUpdateSink
{
private readonly Action<SameGenerationCreateObjectEvents> _apply =
apply ?? throw new ArgumentNullException(nameof(apply));
public void ApplySameGeneration(SameGenerationCreateObjectEvents events) =>
_apply(events);
}
internal readonly record struct LiveEntityOriginInitialization(
bool IsKnown,
IReadOnlyList<uint> AlreadyLoadedLandblocks);
internal interface ILiveEntityWorldOriginCoordinator
{
bool IsKnown { get; }
LiveEntityOriginInitialization TryInitialize(WorldSession.EntitySpawn spawn);
}
/// <summary>
/// Owns the retail CreateObject integration transaction over the canonical
/// <see cref="LiveEntityRuntime"/>. It has no GUID dictionary: every callback
/// captures and revalidates the exact record supplied by the runtime.
///
/// Retail anchors: <c>SmartBox::HandleCreateObject @ 0x00454C80</c> and
/// <c>ACCObjectMaint::CreateObject @ 0x00558870</c>.
/// </summary>
internal sealed class LiveEntityHydrationController
{
private readonly LiveEntityRuntime _runtime;
private readonly ClientObjectTable _objects;
private readonly object _datLock;
private readonly ILiveEntityProjectionMaterializer _materializer;
private readonly ILiveEntitySpawnRelationshipSink _relationships;
private readonly ILiveEntityReadyPublisher _ready;
private readonly ILiveEntityWorldOriginCoordinator _origin;
private readonly ILiveEntityNetworkUpdateSink _networkUpdates;
private readonly Action<uint, AcceptedPhysicsTimestamps> _publishTimestamps;
private readonly Func<uint> _playerGuid;
private readonly Action<string>? _diagnostic;
public LiveEntityHydrationController(
LiveEntityRuntime runtime,
ClientObjectTable objects,
object datLock,
ILiveEntityProjectionMaterializer materializer,
ILiveEntitySpawnRelationshipSink relationships,
ILiveEntityReadyPublisher ready,
ILiveEntityWorldOriginCoordinator origin,
ILiveEntityNetworkUpdateSink networkUpdates,
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
Func<uint> playerGuid,
Action<string>? diagnostic = null)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
_materializer = materializer ?? throw new ArgumentNullException(nameof(materializer));
_relationships = relationships ?? throw new ArgumentNullException(nameof(relationships));
_ready = ready ?? throw new ArgumentNullException(nameof(ready));
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
_networkUpdates = networkUpdates ?? throw new ArgumentNullException(nameof(networkUpdates));
_publishTimestamps = publishTimestamps
?? throw new ArgumentNullException(nameof(publishTimestamps));
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
_diagnostic = diagnostic;
}
public void OnCreate(WorldSession.EntitySpawn spawn)
{
// DatCollection uses one mutable reader cursor shared with streaming.
// Registration, canonical reread, and projection construction remain
// one update-thread transaction under that lock.
lock (_datLock)
{
LiveEntityRegistrationResult registration =
_runtime.RegisterLiveEntity(spawn);
InboundCreateResult result = registration.Inbound;
if (result.Disposition is
AcDream.Core.Physics.CreateObjectTimestampDisposition.StaleGeneration)
{
return;
}
// An accepted retransmit can still be parked behind a retryable
// teardown tombstone. It owns no active record and therefore must
// not mutate retained qualities or route an update tail.
if (registration.Record is not { } record)
return;
ulong createIntegrationVersion = record.CreateIntegrationVersion;
try
{
_publishTimestamps(spawn.Guid, result.Timestamps);
if (_runtime.IsCurrentCreateIntegration(
record,
createIntegrationVersion)
&& ObjectTableWiring.ApplyEntitySpawn(
_objects,
spawn,
replaceGeneration: result.Disposition is
AcDream.Core.Physics.CreateObjectTimestampDisposition.NewGeneration,
accepting: () => _runtime.IsCurrentCreateIntegration(
record,
createIntegrationVersion))
&& _runtime.IsCurrentCreateIntegration(
record,
createIntegrationVersion))
{
if (result.Disposition is
AcDream.Core.Physics.CreateObjectTimestampDisposition.ExistingGeneration)
{
if (registration.LogicalRegistrationCreated)
{
// A converged rollback tombstone can be repaired by
// a same-generation retransmit. Its accepted
// snapshot is already canonical, so construct from
// that snapshot rather than replaying a delta tail
// against owners that did not exist.
ProjectExact(
record,
result.Snapshot,
LiveProjectionPurpose.LogicalRegistration,
expectedCreateIntegrationVersion:
createIntegrationVersion);
}
else
{
if (result.SameGenerationEvents is { } refresh)
_networkUpdates.ApplySameGeneration(refresh);
if (_runtime.IsCurrentCreateIntegration(
record,
createIntegrationVersion)
&& (record.CreateProjectionSynchronizationPending
|| !record.InitialHydrationCompleted))
{
ProjectExact(
record,
record.Snapshot,
record.CreateProjectionSynchronizationPending
? LiveProjectionPurpose.CreateSupersessionRecovery
: LiveProjectionPurpose.SpatialRecovery,
expectedCreateIntegrationVersion:
createIntegrationVersion);
}
}
}
else
{
ProjectExact(
record,
result.Snapshot,
LiveProjectionPurpose.LogicalRegistration,
expectedCreateIntegrationVersion:
createIntegrationVersion);
}
}
}
catch (Exception applyFailure)
when (registration.PriorGenerationCleanupFailure is not null)
{
throw new AggregateException(
$"Live entity 0x{spawn.Guid:X8} replacement cleanup and installation both failed.",
registration.PriorGenerationCleanupFailure,
applyFailure);
}
if (registration.PriorGenerationCleanupFailure is { } cleanupFailure)
{
throw new AggregateException(
$"Prior incarnation of live entity 0x{spawn.Guid:X8} failed teardown after its replacement was installed.",
cleanupFailure);
}
}
}
/// <summary>
/// Reprojects retained live objects after their landblock is loaded. An
/// fully hydrated record only rebuckets. A retained partial projection
/// resumes its exact initial transaction without registering logical
/// mesh/script resources a second time.
/// </summary>
public void OnLandblockLoaded(uint loadedLandblockId)
{
if (_runtime.Count == 0)
return;
uint canonical = (loadedLandblockId & 0xFFFF0000u) | 0xFFFFu;
LiveEntityRecord[] records = _runtime.Records
.Where(record => (record.ServerGuid != _playerGuid()
|| !record.InitialHydrationCompleted
|| record.CreateProjectionSynchronizationPending)
&& record.ProjectionCellId != 0
&& ((record.ProjectionCellId & 0xFFFF0000u) | 0xFFFFu) == canonical
&& record.Snapshot.SetupTableId is not null)
.ToArray();
if (records.Length == 0)
return;
int projected = 0;
lock (_datLock)
{
foreach (LiveEntityRecord record in records)
{
if (!_runtime.IsCurrentRecord(record))
continue;
if (record.CreateProjectionSynchronizationPending)
{
if (ProjectExact(
record,
record.Snapshot,
LiveProjectionPurpose.CreateSupersessionRecovery))
{
projected++;
}
}
else if (record.WorldEntity is not null
&& record.InitialHydrationCompleted)
{
if (_runtime.RebucketLiveEntity(
record.ServerGuid,
record.ProjectionCellId))
{
projected++;
}
}
else if (ProjectExact(
record,
record.Snapshot,
LiveProjectionPurpose.SpatialRecovery))
{
projected++;
}
}
}
if (projected > 0)
{
_diagnostic?.Invoke(
$"live: re-projected {projected} server object(s) into landblock 0x{loadedLandblockId:X8}");
}
}
public bool EnsureWorldOrigin(
LiveEntityRecord expectedRecord,
ulong positionAuthorityVersion,
WorldSession.EntitySpawn acceptedSpawn)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
lock (_datLock)
{
if (!_runtime.IsCurrentPositionAuthority(
expectedRecord,
positionAuthorityVersion))
{
return false;
}
InitializeOriginAndRecoverLoaded(acceptedSpawn);
return _runtime.IsCurrentPositionAuthority(
expectedRecord,
positionAuthorityVersion);
}
}
public bool RecoverProjection(
LiveEntityRecord expectedRecord,
ulong positionAuthorityVersion,
WorldSession.EntitySpawn acceptedSpawn)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
lock (_datLock)
{
if (!_runtime.IsCurrentPositionAuthority(
expectedRecord,
positionAuthorityVersion))
{
return false;
}
return ProjectExact(
expectedRecord,
acceptedSpawn,
expectedRecord.CreateProjectionSynchronizationPending
? LiveProjectionPurpose.CreateSupersessionRecovery
: LiveProjectionPurpose.SpatialRecovery)
&& _runtime.IsCurrentPositionAuthority(
expectedRecord,
positionAuthorityVersion);
}
}
public bool ApplyAppearance(
LiveEntityRecord expectedRecord,
WorldSession.EntitySpawn acceptedSpawn,
LiveEntityAppearanceUpdateState? appearanceUpdate)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
lock (_datLock)
{
return ProjectExact(
expectedRecord,
acceptedSpawn,
appearanceUpdate is null
? LiveProjectionPurpose.SpatialRecovery
: LiveProjectionPurpose.AppearanceMutation,
appearanceUpdate);
}
}
public bool OnEntityReady(LiveEntityReadyCandidate candidate)
{
if (!PublishReady(
candidate.Record,
candidate.CreateIntegrationVersion))
{
return false;
}
return !candidate.Record.CreateProjectionSynchronizationPending
|| _runtime.TryCompleteCreateProjectionSynchronization(
candidate.Record,
candidate.CreateIntegrationVersion);
}
public void ResetSessionState() => _materializer.ResetSessionState();
private bool ProjectExact(
LiveEntityRecord expectedRecord,
WorldSession.EntitySpawn acceptedSpawn,
LiveProjectionPurpose purpose,
LiveEntityAppearanceUpdateState? appearanceUpdate = null,
ulong? expectedCreateIntegrationVersion = null)
{
while (true)
{
ulong createIntegrationVersion = expectedCreateIntegrationVersion
?? expectedRecord.CreateIntegrationVersion;
if (purpose is LiveProjectionPurpose.CreateSupersessionRecovery
&& !_runtime.TryMarkCreateProjectionSynchronizationPending(
expectedRecord,
createIntegrationVersion))
{
return false;
}
if (!_runtime.TryBeginProjectionHydration(
expectedRecord,
createIntegrationVersion))
{
return false;
}
bool retry;
bool result;
try
{
result = ProjectExactOnce(
expectedRecord,
acceptedSpawn,
purpose,
appearanceUpdate,
createIntegrationVersion);
}
finally
{
retry = _runtime.EndProjectionHydration(expectedRecord);
}
if (!retry || !_runtime.IsCurrentRecord(expectedRecord))
return result;
// A fresher same-incarnation CreateObject arrived while this
// transaction owned the hydration edge. Resume in-place from the
// newest canonical snapshot; logical resources remain registered.
acceptedSpawn = expectedRecord.Snapshot;
purpose = LiveProjectionPurpose.CreateSupersessionRecovery;
appearanceUpdate = null;
expectedCreateIntegrationVersion =
expectedRecord.CreateIntegrationVersion;
}
}
private bool ProjectExactOnce(
LiveEntityRecord expectedRecord,
WorldSession.EntitySpawn acceptedSpawn,
LiveProjectionPurpose purpose,
LiveEntityAppearanceUpdateState? appearanceUpdate,
ulong createIntegrationVersion)
{
_relationships.OnSpawn(acceptedSpawn);
if (!_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion))
return false;
// A queued Parent event can synchronously mutate the canonical
// Position/Parent snapshot. Never continue from the stale argument.
WorldSession.EntitySpawn canonicalSpawn = expectedRecord.Snapshot;
InitializeOriginAndRecoverLoaded(canonicalSpawn);
if (!_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion)
|| !_origin.IsKnown)
{
return false;
}
// Retail's same-instance CreateObject branch completes a POSITION_TS
// with no world Position through DoParentEvent or DoPickupEvent, not
// through CPhysicsObj::enter_world. The relationship/update owners
// above have already committed that cell-less state. An attached
// child discharges the obligation at its exact EntityReady edge;
// pickup completes here after exact projection withdrawal.
if (canonicalSpawn.Position is null)
{
if (canonicalSpawn.ParentGuid is not null and not 0)
{
if (expectedRecord.InitialHydrationCompleted
&& !expectedRecord.CreateProjectionSynchronizationPending)
{
return _runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion);
}
if (expectedRecord.WorldEntity is null
|| expectedRecord.ProjectionKind is not
LiveEntityProjectionKind.Attached
|| !expectedRecord.IsSpatiallyProjected
|| !PublishReady(
expectedRecord,
createIntegrationVersion))
{
return false;
}
return !expectedRecord.CreateProjectionSynchronizationPending
|| _runtime.TryCompleteCreateProjectionSynchronization(
expectedRecord,
createIntegrationVersion);
}
if (expectedRecord.FullCellId != 0
|| expectedRecord.IsSpatiallyProjected)
{
return false;
}
if (expectedRecord.WorldEntity is null)
{
// A pickup can supersede initial hydration before a render
// projection exists. The logical snapshot/table state is
// already canonical; ready remains false so a later world
// Position still constructs and publishes the first owner.
return !expectedRecord.CreateProjectionSynchronizationPending
|| _runtime.TryCompleteCreateProjectionSynchronization(
expectedRecord,
createIntegrationVersion);
}
if (!PublishReady(
expectedRecord,
createIntegrationVersion))
{
return false;
}
return !expectedRecord.CreateProjectionSynchronizationPending
|| _runtime.TryCompleteCreateProjectionSynchronization(
expectedRecord,
createIntegrationVersion);
}
bool materialized = _materializer.TryMaterialize(
expectedRecord,
expectedRecord.Snapshot,
purpose,
createIntegrationVersion,
appearanceUpdate);
if (!materialized
|| !_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion)
|| purpose is LiveProjectionPurpose.AppearanceMutation)
{
return materialized;
}
if (!PublishReady(
expectedRecord,
createIntegrationVersion))
{
return false;
}
return purpose is not LiveProjectionPurpose.CreateSupersessionRecovery
|| _runtime.TryCompleteCreateProjectionSynchronization(
expectedRecord,
createIntegrationVersion);
}
private bool PublishReady(
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion)
{
if (!_runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion)
|| !_ready.Publish(expectedRecord)
|| !_runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion))
{
return false;
}
return _runtime.TryMarkInitialHydrationCompleted(
expectedRecord,
expectedCreateIntegrationVersion)
&& _runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion);
}
private void InitializeOriginAndRecoverLoaded(
WorldSession.EntitySpawn canonicalSpawn)
{
LiveEntityOriginInitialization initialization =
_origin.TryInitialize(canonicalSpawn);
if (!initialization.IsKnown)
return;
// Preserve SmartBox's blocking world-entry ordering: already-resident
// cells are made available before the outer CreateObject completes.
for (int i = 0; i < initialization.AlreadyLoadedLandblocks.Count; i++)
OnLandblockLoaded(initialization.AlreadyLoadedLandblocks[i]);
}
}

View file

@ -0,0 +1,150 @@
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.Core.Net;
namespace AcDream.App.World;
internal sealed class DelegateLiveEntitySpawnRelationshipSink(
Action<WorldSession.EntitySpawn> onSpawn) : ILiveEntitySpawnRelationshipSink
{
private readonly Action<WorldSession.EntitySpawn> _onSpawn =
onSpawn ?? throw new ArgumentNullException(nameof(onSpawn));
public void OnSpawn(WorldSession.EntitySpawn spawn) => _onSpawn(spawn);
}
internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
{
private readonly LiveEntityRuntime _runtime;
private readonly Func<LiveEntityRecord, bool> _prepareEffects;
private readonly Func<LiveEntityRecord, bool> _presentState;
private readonly Func<LiveEntityRecord, bool> _replayEffects;
public LiveEntityReadyPublisher(
LiveEntityRuntime runtime,
EntityEffectController effects,
LiveEntityPresentationController presentation)
: this(
runtime,
record => effects.PrepareLiveEntityOwner(record.ServerGuid),
record => presentation.OnLiveEntityReady(record.ServerGuid),
record => effects.ReplayPendingForLiveEntity(record.ServerGuid))
{
ArgumentNullException.ThrowIfNull(effects);
ArgumentNullException.ThrowIfNull(presentation);
}
internal LiveEntityReadyPublisher(
LiveEntityRuntime runtime,
Func<LiveEntityRecord, bool> prepareEffects,
Func<LiveEntityRecord, bool> presentState,
Func<LiveEntityRecord, bool> replayEffects)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_prepareEffects = prepareEffects
?? throw new ArgumentNullException(nameof(prepareEffects));
_presentState = presentState
?? throw new ArgumentNullException(nameof(presentState));
_replayEffects = replayEffects
?? throw new ArgumentNullException(nameof(replayEffects));
}
public bool Publish(LiveEntityRecord expectedRecord)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
ulong createIntegrationVersion = expectedRecord.CreateIntegrationVersion;
if (!_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion)
|| !_prepareEffects(expectedRecord)
|| !_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion))
return false;
if (!_presentState(expectedRecord)
|| !_runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion))
{
return false;
}
return _replayEffects(expectedRecord)
&& _runtime.IsCurrentCreateIntegration(
expectedRecord,
createIntegrationVersion);
}
}
/// <summary>
/// Converts the first accepted local-player Position into the shared streaming
/// origin. Retail <c>SmartBox::UseTime @ 0x00455410</c> blocks completion until
/// destination cells are ready; already-resident landblocks are returned to
/// hydration for the same synchronous recovery edge.
/// </summary>
internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginCoordinator
{
private readonly LiveWorldOriginState _origin;
private readonly StreamingController _streaming;
private readonly GpuWorldState _worldState;
private readonly WorldRevealCoordinator _worldReveal;
private readonly Func<uint> _playerGuid;
private readonly Func<uint, bool> _isSealedDungeonCell;
private readonly Action<string>? _diagnostic;
public LiveEntityWorldOriginCoordinator(
LiveWorldOriginState origin,
StreamingController streaming,
GpuWorldState worldState,
WorldRevealCoordinator worldReveal,
Func<uint> playerGuid,
Func<uint, bool> isSealedDungeonCell,
Action<string>? diagnostic = null)
{
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
_isSealedDungeonCell = isSealedDungeonCell
?? throw new ArgumentNullException(nameof(isSealedDungeonCell));
_diagnostic = diagnostic;
}
public bool IsKnown => _origin.IsKnown;
public LiveEntityOriginInitialization TryInitialize(
WorldSession.EntitySpawn spawn)
{
if (spawn.Guid != _playerGuid()
|| spawn.Position is not { } position)
{
return new(_origin.IsKnown, Array.Empty<uint>());
}
int lbX = (int)((position.LandblockId >> 24) & 0xFFu);
int lbY = (int)((position.LandblockId >> 16) & 0xFFu);
int oldCenterX = _origin.CenterX;
int oldCenterY = _origin.CenterY;
if (!_origin.TryInitialize(lbX, lbY))
return new(true, Array.Empty<uint>());
if (lbX != oldCenterX || lbY != oldCenterY)
{
_diagnostic?.Invoke(
$"live: first player position — recentering streaming from ({oldCenterX},{oldCenterY}) "
+ $"to ({lbX},{lbY}) @0x{position.LandblockId:X8}");
}
_streaming.InitializeKnownLoginCenter(
lbX,
lbY,
isSealedDungeon: _isSealedDungeonCell(position.LandblockId));
_worldReveal.Begin(WorldRevealKind.Login);
return new(
true,
_worldState.LoadedLandblockIds.ToArray());
}
}

View file

@ -258,7 +258,32 @@ public sealed class LiveEntityRecord
internal ulong VectorAuthorityVersion { get; private set; }
internal ulong VelocityAuthorityVersion { get; private set; }
internal ulong MovementAuthorityVersion { get; private set; }
/// <summary>
/// Advances for every accepted CreateObject affecting this incarnation,
/// including a fresher same-INSTANCE_TS retransmit. App integration uses
/// it to distinguish two transactions that intentionally share identity.
/// </summary>
internal ulong CreateIntegrationVersion { get; private set; } = 1UL;
public bool WorldSpawnPublished { get; internal set; }
/// <summary>
/// True only after the incarnation's projection, collision/animation
/// components, effect owner, state presentation, and pending-effect replay
/// have crossed the initial ready barrier. A non-null
/// <see cref="WorldEntity"/> alone is not sufficient: failures after
/// resource registration retain the same projection for exact retry.
/// </summary>
public bool InitialHydrationCompleted { get; internal set; }
/// <summary>
/// A fresher same-incarnation CreateObject interrupted projection after
/// logical ownership had already been established. The retained entity
/// must replay the newest canonical appearance/current snapshot/animation
/// before ordinary rebucketing can resume. This obligation deliberately
/// survives a failed recovery attempt.
/// </summary>
internal bool CreateProjectionSynchronizationPending { get; set; }
internal bool ProjectionHydrationInProgress { get; set; }
internal ulong ProjectionHydrationCreateVersion { get; set; }
internal bool ProjectionHydrationRetryRequested { get; set; }
public LiveEntityProjectionKind ProjectionKind { get; internal set; }
internal bool RuntimeComponentsTeardownCompleted { get; set; }
internal LiveEntityTeardownPlan? RuntimeComponentTeardownPlan { get; set; }
@ -320,6 +345,7 @@ public sealed class LiveEntityRecord
VectorAuthorityVersion++;
VelocityAuthorityVersion++;
MovementAuthorityVersion++;
CreateIntegrationVersion++;
}
internal void SetChildNoDraw(bool noDraw)
@ -2031,6 +2057,111 @@ public sealed class LiveEntityRuntime
return true;
}
internal bool TryMarkInitialHydrationCompleted(
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!IsCurrentRecord(expectedRecord)
|| expectedRecord.CreateIntegrationVersion != expectedCreateIntegrationVersion
|| expectedRecord.WorldEntity is null
|| !expectedRecord.ResourcesRegistered)
{
return false;
}
expectedRecord.InitialHydrationCompleted = true;
return true;
}
internal bool TryMarkCreateProjectionSynchronizationPending(
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion))
{
return false;
}
expectedRecord.CreateProjectionSynchronizationPending = true;
return true;
}
internal bool TryCompleteCreateProjectionSynchronization(
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion))
{
return false;
}
expectedRecord.CreateProjectionSynchronizationPending = false;
return true;
}
internal bool TryBeginProjectionHydration(
LiveEntityRecord expectedRecord,
ulong? expectedCreateIntegrationVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!IsCurrentRecord(expectedRecord)
|| (expectedCreateIntegrationVersion is { } expected
&& expectedRecord.CreateIntegrationVersion != expected))
{
return false;
}
if (expectedRecord.ProjectionHydrationInProgress)
{
if (expectedRecord.CreateIntegrationVersion
!= expectedRecord.ProjectionHydrationCreateVersion)
{
expectedRecord.ProjectionHydrationRetryRequested = true;
}
return false;
}
expectedRecord.ProjectionHydrationInProgress = true;
expectedRecord.ProjectionHydrationCreateVersion =
expectedRecord.CreateIntegrationVersion;
expectedRecord.ProjectionHydrationRetryRequested = false;
return true;
}
internal bool EndProjectionHydration(LiveEntityRecord expectedRecord)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
bool retry = IsCurrentRecord(expectedRecord)
&& (expectedRecord.ProjectionHydrationRetryRequested
|| expectedRecord.CreateIntegrationVersion
!= expectedRecord.ProjectionHydrationCreateVersion);
if (retry)
{
// Persist the exact replay obligation at the point version drift
// is observed. The stale projection body may be unwinding through
// an exception and therefore never reach the controller's next
// retry-loop iteration.
expectedRecord.CreateProjectionSynchronizationPending = true;
}
expectedRecord.ProjectionHydrationInProgress = false;
expectedRecord.ProjectionHydrationCreateVersion = 0UL;
expectedRecord.ProjectionHydrationRetryRequested = false;
return retry;
}
internal bool IsCurrentCreateIntegration(
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion) =>
IsCurrentRecord(expectedRecord)
&& expectedRecord.CreateIntegrationVersion == expectedCreateIntegrationVersion;
public void Clear()
{
if (_isClearing || _isRegisteringResources)
@ -2586,6 +2717,11 @@ public sealed class LiveEntityRuntime
record.IsSpatiallyProjected = false;
record.IsSpatiallyVisible = false;
record.WorldSpawnPublished = false;
record.InitialHydrationCompleted = false;
record.CreateProjectionSynchronizationPending = false;
record.ProjectionHydrationInProgress = false;
record.ProjectionHydrationCreateVersion = 0UL;
record.ProjectionHydrationRetryRequested = false;
record.WorldEntity = null;
}
finally

View file

@ -101,16 +101,31 @@ public static class ObjectTableWiring
/// owning runtime accepts its physics generation. Internal for a
/// socket-free conformance test of the subscription body.
/// </summary>
public static void ApplyEntitySpawn(
public static bool ApplyEntitySpawn(
ClientObjectTable table,
WorldSession.EntitySpawn spawn,
bool replaceGeneration = false)
bool replaceGeneration = false,
Func<bool>? accepting = null)
{
ArgumentNullException.ThrowIfNull(table);
if (accepting?.Invoke() == false)
return false;
WeenieData data = ToWeenieData(spawn);
if (replaceGeneration)
table.ReplaceGeneration(data, spawn.InstanceSequence);
{
return table.ReplaceGeneration(
data,
spawn.InstanceSequence,
accepting) is not null;
}
else
table.Ingest(data);
// Ingest publishes ObjectAdded/ObjectUpdated synchronously. A nested
// replacement wins; report the invalidated outer application so its
// remaining CreateObject tail cannot run.
return accepting?.Invoke() != false;
}
/// <summary>

View file

@ -576,7 +576,10 @@ public sealed class ClientObjectTable
/// Atomically replaces the retained qualities for a newer server object
/// incarnation while publishing a generation-specific teardown event.
/// </summary>
public ClientObject ReplaceGeneration(WeenieData data, ushort generation)
public ClientObject? ReplaceGeneration(
WeenieData data,
ushort generation,
Func<bool>? canInstallReplacement = null)
{
RemoveCore(
data.Guid,
@ -584,6 +587,13 @@ public sealed class ClientObjectTable
generation,
notifyObjectRemoved: true);
// Removal observers may synchronously accept a newer CreateObject for
// this GUID. The outer generation must not resume and merge its stale
// qualities into that replacement. The authoritative live-identity
// owner supplies the exact-incarnation predicate.
if (canInstallReplacement?.Invoke() == false)
return null;
return Ingest(data);
}