refactor(world): extract live entity hydration
This commit is contained in:
parent
27dcd0ecb7
commit
d10c5f2d54
17 changed files with 4310 additions and 1269 deletions
1119
src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs
Normal file
1119
src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
|
|
|
|||
613
src/AcDream.App/World/LiveEntityHydrationController.cs
Normal file
613
src/AcDream.App/World/LiveEntityHydrationController.cs
Normal 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]);
|
||||
}
|
||||
}
|
||||
150
src/AcDream.App/World/LiveEntityHydrationPorts.cs
Normal file
150
src/AcDream.App/World/LiveEntityHydrationPorts.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue