refactor(app): key live projections by runtime identity
Move materialized live-object sidecars and presentation worksets to exact RuntimeEntityKey ownership. Runtime remains the only GUID/incarnation/local-ID authority while hydration, animation, effects, lights, equipped children, renderer resources, visibility, liveness, and teardown resolve exact projection identities. Preserve synchronous callbacks, local-ID allocation order, and current rendering behavior.
This commit is contained in:
parent
e18df84437
commit
420e5eea70
73 changed files with 2939 additions and 1715 deletions
15
src/AcDream.App/World/ILiveEntityRadarSource.cs
Normal file
15
src/AcDream.App/World/ILiveEntityRadarSource.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Allocation-free live-projection query used by the radar. Implementations
|
||||
/// expose current presentation state without publishing a second GUID identity
|
||||
/// map.
|
||||
/// </summary>
|
||||
public interface ILiveEntityRadarSource
|
||||
{
|
||||
bool TryGetMaterialized(uint serverGuid, out WorldEntity entity);
|
||||
bool TryGetVisible(uint serverGuid, out WorldEntity entity);
|
||||
void CopyVisibleTo(List<KeyValuePair<uint, WorldEntity>> destination);
|
||||
}
|
||||
|
|
@ -78,9 +78,9 @@ internal sealed class LiveEntityDeletionController : ILiveEntityPruneSink
|
|||
public bool Prune(LiveEntityPruneCandidate candidate)
|
||||
{
|
||||
if (!_runtime.TryGetRecord(
|
||||
candidate.ServerGuid,
|
||||
candidate.Key,
|
||||
out LiveEntityRecord record)
|
||||
|| record.Generation != candidate.Generation)
|
||||
|| record.ServerGuid != candidate.ServerGuid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ internal enum LiveProjectionPurpose
|
|||
internal interface ILiveEntityProjectionMaterializer
|
||||
{
|
||||
bool TryMaterialize(
|
||||
LiveEntityRecord expectedRecord,
|
||||
RuntimeEntityRecord expectedCanonical,
|
||||
WorldSession.EntitySpawn canonicalSpawn,
|
||||
LiveProjectionPurpose purpose,
|
||||
ulong expectedCreateIntegrationVersion,
|
||||
|
|
@ -181,6 +181,15 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
private readonly LiveEntityDeletionController _deletion;
|
||||
private readonly DormantLiveEntityStore _dormant;
|
||||
private readonly Action<string>? _diagnostic;
|
||||
private readonly Dictionary<RuntimeEntityRecord, CanonicalProjectionOperation>
|
||||
_projectionOperations =
|
||||
new(ReferenceEqualityComparer.Instance);
|
||||
|
||||
private sealed class CanonicalProjectionOperation
|
||||
{
|
||||
public ulong CreateIntegrationVersion { get; set; }
|
||||
public bool RetryRequested { get; set; }
|
||||
}
|
||||
|
||||
public LiveEntityHydrationController(
|
||||
LiveEntityRuntime runtime,
|
||||
|
|
@ -264,17 +273,17 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
// dormant snapshot until an active record actually accepts
|
||||
// ownership; otherwise a transient teardown failure would discard
|
||||
// the only ACE revisit source.
|
||||
if (registration.Record is not { } record)
|
||||
if (registration.Canonical is not { } canonical)
|
||||
return;
|
||||
|
||||
_dormant.RemoveThroughAcceptedCreate(spawn);
|
||||
ulong createIntegrationVersion = record.CreateIntegrationVersion;
|
||||
ulong createIntegrationVersion = canonical.CreateIntegrationVersion;
|
||||
|
||||
try
|
||||
{
|
||||
_timestamps.Publish(spawn.Guid, result.Timestamps);
|
||||
if (_runtime.IsCurrentCreateIntegration(
|
||||
record,
|
||||
canonical,
|
||||
createIntegrationVersion)
|
||||
&& ObjectTableWiring.ApplyEntitySpawn(
|
||||
_objects,
|
||||
|
|
@ -283,10 +292,10 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
AcDream.Core.Physics.CreateObjectTimestampDisposition.NewGeneration
|
||||
|| dormantDisposition is DormantCreateDisposition.NewGeneration,
|
||||
accepting: () => _runtime.IsCurrentCreateIntegration(
|
||||
record,
|
||||
canonical,
|
||||
createIntegrationVersion))
|
||||
&& _runtime.IsCurrentCreateIntegration(
|
||||
record,
|
||||
canonical,
|
||||
createIntegrationVersion))
|
||||
{
|
||||
if (result.Disposition is
|
||||
|
|
@ -300,7 +309,7 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
// that snapshot rather than replaying a delta tail
|
||||
// against owners that did not exist.
|
||||
ProjectExact(
|
||||
record,
|
||||
canonical,
|
||||
result.Snapshot,
|
||||
LiveProjectionPurpose.LogicalRegistration,
|
||||
expectedCreateIntegrationVersion:
|
||||
|
|
@ -312,15 +321,23 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
_networkUpdates.ApplySameGeneration(refresh);
|
||||
|
||||
if (_runtime.IsCurrentCreateIntegration(
|
||||
record,
|
||||
createIntegrationVersion)
|
||||
&& (record.CreateProjectionSynchronizationPending
|
||||
|| !record.InitialHydrationCompleted))
|
||||
canonical,
|
||||
createIntegrationVersion))
|
||||
{
|
||||
_runtime.TryGetProjection(
|
||||
canonical,
|
||||
out LiveEntityRecord? projection);
|
||||
if (projection is not null
|
||||
&& !projection.CreateProjectionSynchronizationPending
|
||||
&& projection.InitialHydrationCompleted)
|
||||
{
|
||||
goto AppearanceSynchronization;
|
||||
}
|
||||
ProjectExact(
|
||||
record,
|
||||
record.Snapshot,
|
||||
record.CreateProjectionSynchronizationPending
|
||||
canonical,
|
||||
canonical.Snapshot,
|
||||
projection?.CreateProjectionSynchronizationPending
|
||||
is true
|
||||
? LiveProjectionPurpose.CreateSupersessionRecovery
|
||||
: LiveProjectionPurpose.SpatialRecovery,
|
||||
expectedCreateIntegrationVersion:
|
||||
|
|
@ -331,19 +348,22 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
else
|
||||
{
|
||||
ProjectExact(
|
||||
record,
|
||||
canonical,
|
||||
result.Snapshot,
|
||||
LiveProjectionPurpose.LogicalRegistration,
|
||||
expectedCreateIntegrationVersion:
|
||||
createIntegrationVersion);
|
||||
}
|
||||
|
||||
if (_runtime.IsCurrentRecord(record)
|
||||
&& record.AppearanceProjectionSynchronizationPending)
|
||||
AppearanceSynchronization:
|
||||
if (_runtime.TryGetProjection(
|
||||
canonical,
|
||||
out LiveEntityRecord? currentProjection)
|
||||
&& currentProjection.AppearanceProjectionSynchronizationPending)
|
||||
{
|
||||
SynchronizeAppearance(
|
||||
record,
|
||||
record.ObjDescAuthorityVersion);
|
||||
currentProjection,
|
||||
currentProjection.ObjDescAuthorityVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -375,12 +395,25 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
{
|
||||
lock (_datLock)
|
||||
{
|
||||
if (!_runtime.TryApplyObjDesc(update, out _)
|
||||
|| !_runtime.TryGetRecord(update.Guid, out LiveEntityRecord record))
|
||||
if (!_runtime.TryApplyObjDesc(update, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_runtime.TryGetRecord(
|
||||
update.Guid,
|
||||
out LiveEntityRecord record))
|
||||
{
|
||||
return _runtime.TryGetCanonical(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord canonical)
|
||||
&& ProjectExact(
|
||||
canonical,
|
||||
canonical.Snapshot,
|
||||
LiveProjectionPurpose.SpatialRecovery,
|
||||
canonical.CreateIntegrationVersion);
|
||||
}
|
||||
|
||||
return SynchronizeAppearance(
|
||||
record,
|
||||
record.ObjDescAuthorityVersion);
|
||||
|
|
@ -466,42 +499,64 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
if (_runtime.Count == 0)
|
||||
return;
|
||||
|
||||
uint canonical = (loadedLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
LiveEntityRecord[] records = _runtime.Records
|
||||
.Where(record => (record.ServerGuid != _identity.ServerGuid
|
||||
|| !record.InitialHydrationCompleted
|
||||
|| record.CreateProjectionSynchronizationPending
|
||||
|| record.AppearanceProjectionSynchronizationPending)
|
||||
&& record.ProjectionCellId != 0
|
||||
&& ((record.ProjectionCellId & 0xFFFF0000u) | 0xFFFFu) == canonical
|
||||
&& record.Snapshot.SetupTableId is not null)
|
||||
.ToArray();
|
||||
if (records.Length == 0)
|
||||
uint canonicalLandblock =
|
||||
(loadedLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
var candidates = new List<RuntimeEntityRecord>();
|
||||
foreach (RuntimeEntityRecord candidate in _runtime.CanonicalRecords)
|
||||
{
|
||||
_runtime.TryGetProjection(
|
||||
candidate,
|
||||
out LiveEntityRecord? projection);
|
||||
if (candidate.ServerGuid == _identity.ServerGuid
|
||||
&& projection is not null
|
||||
&& projection.InitialHydrationCompleted
|
||||
&& !projection.CreateProjectionSynchronizationPending
|
||||
&& !projection.AppearanceProjectionSynchronizationPending)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uint projectionCellId = projection?.ProjectionCellId
|
||||
?? candidate.Snapshot.Position?.LandblockId
|
||||
?? candidate.FullCellId;
|
||||
if (projectionCellId != 0
|
||||
&& ((projectionCellId & 0xFFFF0000u) | 0xFFFFu)
|
||||
== canonicalLandblock
|
||||
&& candidate.Snapshot.SetupTableId is not null)
|
||||
{
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
}
|
||||
if (candidates.Count == 0)
|
||||
return;
|
||||
|
||||
int projected = 0;
|
||||
lock (_datLock)
|
||||
{
|
||||
foreach (LiveEntityRecord record in records)
|
||||
foreach (RuntimeEntityRecord candidate in candidates)
|
||||
{
|
||||
if (!_runtime.IsCurrentRecord(record))
|
||||
if (!_runtime.IsCurrentCanonical(candidate))
|
||||
continue;
|
||||
|
||||
ulong projectedObjDescVersion = record.ObjDescAuthorityVersion;
|
||||
_runtime.TryGetProjection(
|
||||
candidate,
|
||||
out LiveEntityRecord? record);
|
||||
ulong projectedObjDescVersion =
|
||||
candidate.ObjDescAuthorityVersion;
|
||||
bool projectedCanonicalAppearance = false;
|
||||
|
||||
if (record.CreateProjectionSynchronizationPending)
|
||||
if (record?.CreateProjectionSynchronizationPending is true)
|
||||
{
|
||||
if (ProjectExact(
|
||||
record,
|
||||
record.Snapshot,
|
||||
candidate,
|
||||
candidate.Snapshot,
|
||||
LiveProjectionPurpose.CreateSupersessionRecovery))
|
||||
{
|
||||
projected++;
|
||||
projectedCanonicalAppearance = true;
|
||||
}
|
||||
}
|
||||
else if (record.WorldEntity is not null
|
||||
else if (record?.WorldEntity is not null
|
||||
&& record.InitialHydrationCompleted)
|
||||
{
|
||||
if (_runtime.RebucketLiveEntity(
|
||||
|
|
@ -512,15 +567,17 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
}
|
||||
}
|
||||
else if (ProjectExact(
|
||||
record,
|
||||
record.Snapshot,
|
||||
candidate,
|
||||
candidate.Snapshot,
|
||||
LiveProjectionPurpose.SpatialRecovery))
|
||||
{
|
||||
projected++;
|
||||
projectedCanonicalAppearance = true;
|
||||
}
|
||||
|
||||
if (projectedCanonicalAppearance
|
||||
_runtime.TryGetProjection(candidate, out record);
|
||||
if (record is not null
|
||||
&& projectedCanonicalAppearance
|
||||
&& record.AppearanceProjectionSynchronizationPending)
|
||||
{
|
||||
CompleteAppearanceProjectionSynchronization(
|
||||
|
|
@ -528,7 +585,8 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
projectedObjDescVersion);
|
||||
}
|
||||
|
||||
if (_runtime.IsCurrentRecord(record)
|
||||
if (record is not null
|
||||
&& _runtime.IsCurrentRecord(record)
|
||||
&& record.AppearanceProjectionSynchronizationPending
|
||||
&& SynchronizeAppearance(
|
||||
record,
|
||||
|
|
@ -684,7 +742,7 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
else
|
||||
{
|
||||
published = _materializer.TryMaterialize(
|
||||
expectedRecord,
|
||||
expectedRecord.Canonical,
|
||||
expectedRecord.Snapshot,
|
||||
LiveProjectionPurpose.AppearanceMutation,
|
||||
expectedRecord.CreateIntegrationVersion,
|
||||
|
|
@ -715,78 +773,138 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
LiveEntityRecord expectedRecord,
|
||||
WorldSession.EntitySpawn acceptedSpawn,
|
||||
LiveProjectionPurpose purpose,
|
||||
ulong? expectedCreateIntegrationVersion = null) =>
|
||||
ProjectExact(
|
||||
expectedRecord.Canonical,
|
||||
acceptedSpawn,
|
||||
purpose,
|
||||
expectedCreateIntegrationVersion);
|
||||
|
||||
private bool ProjectExact(
|
||||
RuntimeEntityRecord expectedCanonical,
|
||||
WorldSession.EntitySpawn acceptedSpawn,
|
||||
LiveProjectionPurpose purpose,
|
||||
ulong? expectedCreateIntegrationVersion = null)
|
||||
{
|
||||
while (true)
|
||||
if (_projectionOperations.TryGetValue(
|
||||
expectedCanonical,
|
||||
out CanonicalProjectionOperation? active))
|
||||
{
|
||||
ulong createIntegrationVersion = expectedCreateIntegrationVersion
|
||||
?? expectedRecord.CreateIntegrationVersion;
|
||||
if (purpose is LiveProjectionPurpose.CreateSupersessionRecovery
|
||||
&& !_runtime.TryMarkCreateProjectionSynchronizationPending(
|
||||
expectedRecord,
|
||||
createIntegrationVersion))
|
||||
if (expectedCanonical.CreateIntegrationVersion
|
||||
!= active.CreateIntegrationVersion)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!_runtime.TryBeginProjectionHydration(
|
||||
expectedRecord,
|
||||
createIntegrationVersion))
|
||||
{
|
||||
return false;
|
||||
active.RetryRequested = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool retry;
|
||||
bool result;
|
||||
try
|
||||
var operation = new CanonicalProjectionOperation();
|
||||
_projectionOperations.Add(expectedCanonical, operation);
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
result = ProjectExactOnce(
|
||||
expectedRecord,
|
||||
ulong createIntegrationVersion = expectedCreateIntegrationVersion
|
||||
?? expectedCanonical.CreateIntegrationVersion;
|
||||
if (!_runtime.IsCurrentCreateIntegration(
|
||||
expectedCanonical,
|
||||
createIntegrationVersion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (purpose is LiveProjectionPurpose.CreateSupersessionRecovery
|
||||
&& _runtime.TryGetProjection(
|
||||
expectedCanonical,
|
||||
out LiveEntityRecord? synchronizedProjection)
|
||||
&& !_runtime.TryMarkCreateProjectionSynchronizationPending(
|
||||
synchronizedProjection,
|
||||
createIntegrationVersion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
operation.RetryRequested = false;
|
||||
operation.CreateIntegrationVersion =
|
||||
createIntegrationVersion;
|
||||
bool result = ProjectExactOnce(
|
||||
expectedCanonical,
|
||||
acceptedSpawn,
|
||||
purpose,
|
||||
createIntegrationVersion);
|
||||
|
||||
bool retry = operation.RetryRequested
|
||||
|| (_runtime.IsCurrentCanonical(expectedCanonical)
|
||||
&& expectedCanonical.CreateIntegrationVersion
|
||||
!= createIntegrationVersion);
|
||||
if (!retry || !_runtime.IsCurrentCanonical(expectedCanonical))
|
||||
return result;
|
||||
|
||||
// A fresher same-incarnation CreateObject or a synchronous
|
||||
// loaded-landblock callback re-entered this exact canonical
|
||||
// construction. Resume immediately from Runtime's newest
|
||||
// state; no projection work is deferred to another frame.
|
||||
if (_runtime.TryGetProjection(
|
||||
expectedCanonical,
|
||||
out LiveEntityRecord? retryProjection))
|
||||
{
|
||||
retryProjection.CreateProjectionSynchronizationPending = true;
|
||||
}
|
||||
acceptedSpawn = expectedCanonical.Snapshot;
|
||||
purpose = LiveProjectionPurpose.CreateSupersessionRecovery;
|
||||
expectedCreateIntegrationVersion =
|
||||
expectedCanonical.CreateIntegrationVersion;
|
||||
}
|
||||
finally
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (_runtime.IsCurrentCanonical(expectedCanonical)
|
||||
&& _runtime.TryGetProjection(
|
||||
expectedCanonical,
|
||||
out LiveEntityRecord? interruptedProjection))
|
||||
{
|
||||
retry = _runtime.EndProjectionHydration(expectedRecord);
|
||||
interruptedProjection.CreateProjectionSynchronizationPending = true;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!_projectionOperations.Remove(expectedCanonical, out var removed)
|
||||
|| !ReferenceEquals(removed, operation))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Canonical projection construction ownership was corrupted.");
|
||||
}
|
||||
|
||||
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;
|
||||
expectedCreateIntegrationVersion =
|
||||
expectedRecord.CreateIntegrationVersion;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ProjectExactOnce(
|
||||
LiveEntityRecord expectedRecord,
|
||||
RuntimeEntityRecord expectedCanonical,
|
||||
WorldSession.EntitySpawn acceptedSpawn,
|
||||
LiveProjectionPurpose purpose,
|
||||
ulong createIntegrationVersion)
|
||||
{
|
||||
_relationships.OnSpawn(acceptedSpawn);
|
||||
if (!_runtime.IsCurrentCreateIntegration(
|
||||
expectedRecord,
|
||||
expectedCanonical,
|
||||
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;
|
||||
WorldSession.EntitySpawn canonicalSpawn = expectedCanonical.Snapshot;
|
||||
InitializeOriginAndRecoverLoaded(canonicalSpawn);
|
||||
if (!_runtime.IsCurrentCreateIntegration(
|
||||
expectedRecord,
|
||||
expectedCanonical,
|
||||
createIntegrationVersion)
|
||||
|| !_origin.IsKnown)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_runtime.TryGetProjection(
|
||||
expectedCanonical,
|
||||
out LiveEntityRecord? expectedRecord);
|
||||
|
||||
// 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
|
||||
|
|
@ -797,6 +915,9 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
{
|
||||
if (canonicalSpawn.ParentGuid is not null and not 0)
|
||||
{
|
||||
if (expectedRecord is null)
|
||||
return false;
|
||||
|
||||
if (expectedRecord.InitialHydrationCompleted
|
||||
&& !expectedRecord.CreateProjectionSynchronizationPending)
|
||||
{
|
||||
|
|
@ -822,19 +943,20 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
createIntegrationVersion);
|
||||
}
|
||||
|
||||
if (expectedRecord.FullCellId != 0
|
||||
|| expectedRecord.IsSpatiallyProjected)
|
||||
if (expectedCanonical.FullCellId != 0
|
||||
|| expectedRecord?.IsSpatiallyProjected is true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedRecord.WorldEntity is null)
|
||||
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
|
||||
return expectedRecord is null
|
||||
|| !expectedRecord.CreateProjectionSynchronizationPending
|
||||
|| _runtime.TryCompleteCreateProjectionSynchronization(
|
||||
expectedRecord,
|
||||
createIntegrationVersion);
|
||||
|
|
@ -854,19 +976,26 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
|||
}
|
||||
|
||||
bool materialized = _materializer.TryMaterialize(
|
||||
expectedRecord,
|
||||
expectedRecord.Snapshot,
|
||||
expectedCanonical,
|
||||
expectedCanonical.Snapshot,
|
||||
purpose,
|
||||
createIntegrationVersion);
|
||||
if (!materialized
|
||||
|| !_runtime.IsCurrentCreateIntegration(
|
||||
expectedRecord,
|
||||
expectedCanonical,
|
||||
createIntegrationVersion)
|
||||
|| purpose is LiveProjectionPurpose.AppearanceMutation)
|
||||
{
|
||||
return materialized;
|
||||
}
|
||||
|
||||
if (!_runtime.TryGetProjection(
|
||||
expectedCanonical,
|
||||
out expectedRecord))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PublishReady(
|
||||
expectedRecord,
|
||||
createIntegrationVersion))
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Protects GUID-keyed runtime state while an older incarnation is draining.
|
||||
/// A teardown tombstone may outlive the active record and a newer generation
|
||||
/// may already own the same server GUID; only state captured by reference may
|
||||
/// be removed unconditionally in that case.
|
||||
/// </summary>
|
||||
internal sealed class LiveEntityIncarnationCleanup
|
||||
{
|
||||
private readonly LiveEntityRecord _owner;
|
||||
private readonly Func<uint, LiveEntityRecord?> _resolveCurrent;
|
||||
|
||||
public LiveEntityIncarnationCleanup(
|
||||
LiveEntityRecord owner,
|
||||
Func<uint, LiveEntityRecord?> resolveCurrent)
|
||||
{
|
||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
_resolveCurrent = resolveCurrent
|
||||
?? throw new ArgumentNullException(nameof(resolveCurrent));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a GUID-scoped mutation only while no replacement incarnation owns
|
||||
/// that GUID. The original record may still be current (projection-only
|
||||
/// withdrawal) or may already have moved into the teardown ledger.
|
||||
/// </summary>
|
||||
public void RunIfNoReplacement(Action mutation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(mutation);
|
||||
LiveEntityRecord? current = _resolveCurrent(_owner.ServerGuid);
|
||||
if (current is null || ReferenceEquals(current, _owner))
|
||||
mutation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a reference-owned dictionary entry only when the entry still
|
||||
/// points at this incarnation's captured owner. This remains safe even if
|
||||
/// a replacement has overwritten the same GUID between plan creation and
|
||||
/// execution.
|
||||
/// </summary>
|
||||
public void RemoveCaptured<T>(
|
||||
IDictionary<uint, T> owners,
|
||||
T captured)
|
||||
where T : class
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(owners);
|
||||
ArgumentNullException.ThrowIfNull(captured);
|
||||
if (owners.TryGetValue(_owner.ServerGuid, out T? current)
|
||||
&& ReferenceEquals(current, captured))
|
||||
{
|
||||
owners.Remove(_owner.ServerGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,25 @@
|
|||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
internal readonly record struct LiveEntityLivenessSample(
|
||||
RuntimeEntityKey Key,
|
||||
uint ServerGuid,
|
||||
ushort Generation,
|
||||
bool IsConservativelyVisible,
|
||||
bool HasNonWorldRetention);
|
||||
|
||||
internal readonly record struct LiveEntityPruneCandidate(
|
||||
RuntimeEntityKey Key,
|
||||
uint ServerGuid,
|
||||
ushort Generation);
|
||||
ushort Generation)
|
||||
{
|
||||
public LiveEntityPruneCandidate(RuntimeEntityKey key, uint serverGuid)
|
||||
: this(key, serverGuid, key.Incarnation)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deadline state for ACE's retained KnownObjects behavior. Retail
|
||||
|
|
@ -23,7 +31,10 @@ internal sealed class LiveEntityLivenessTracker
|
|||
{
|
||||
internal const double DestructionTimeoutSeconds = 25.0;
|
||||
|
||||
private readonly Dictionary<uint, Deadline> _deadlines = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, double> _deadlines = [];
|
||||
private readonly HashSet<RuntimeEntityKey> _present = [];
|
||||
private readonly List<RuntimeEntityKey> _stale = [];
|
||||
private readonly List<LiveEntityPruneCandidate> _due = [];
|
||||
|
||||
internal int DeadlineCount => _deadlines.Count;
|
||||
|
||||
|
|
@ -31,46 +42,44 @@ internal sealed class LiveEntityLivenessTracker
|
|||
double now,
|
||||
IReadOnlyList<LiveEntityLivenessSample> samples)
|
||||
{
|
||||
var present = new HashSet<uint>(samples.Count);
|
||||
var due = new List<LiveEntityPruneCandidate>();
|
||||
_present.Clear();
|
||||
_due.Clear();
|
||||
for (int i = 0; i < samples.Count; i++)
|
||||
{
|
||||
LiveEntityLivenessSample sample = samples[i];
|
||||
present.Add(sample.ServerGuid);
|
||||
_present.Add(sample.Key);
|
||||
if (sample.IsConservativelyVisible || sample.HasNonWorldRetention)
|
||||
{
|
||||
_deadlines.Remove(sample.ServerGuid);
|
||||
_deadlines.Remove(sample.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_deadlines.TryGetValue(sample.ServerGuid, out Deadline deadline)
|
||||
|| deadline.Generation != sample.Generation)
|
||||
if (!_deadlines.TryGetValue(sample.Key, out double expiresAt))
|
||||
{
|
||||
_deadlines[sample.ServerGuid] = new Deadline(
|
||||
sample.Generation,
|
||||
now + DestructionTimeoutSeconds);
|
||||
_deadlines[sample.Key] = now + DestructionTimeoutSeconds;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (deadline.ExpiresAt > now)
|
||||
if (expiresAt > now)
|
||||
continue;
|
||||
|
||||
due.Add(new LiveEntityPruneCandidate(sample.ServerGuid, sample.Generation));
|
||||
_deadlines.Remove(sample.ServerGuid);
|
||||
_due.Add(new LiveEntityPruneCandidate(sample.Key, sample.ServerGuid));
|
||||
_deadlines.Remove(sample.Key);
|
||||
}
|
||||
|
||||
uint[] stale = _deadlines.Keys
|
||||
.Where(guid => !present.Contains(guid))
|
||||
.ToArray();
|
||||
for (int i = 0; i < stale.Length; i++)
|
||||
_deadlines.Remove(stale[i]);
|
||||
_stale.Clear();
|
||||
foreach (RuntimeEntityKey key in _deadlines.Keys)
|
||||
{
|
||||
if (!_present.Contains(key))
|
||||
_stale.Add(key);
|
||||
}
|
||||
for (int i = 0; i < _stale.Count; i++)
|
||||
_deadlines.Remove(_stale[i]);
|
||||
|
||||
return due;
|
||||
return _due;
|
||||
}
|
||||
|
||||
internal void Clear() => _deadlines.Clear();
|
||||
|
||||
private readonly record struct Deadline(ushort Generation, double ExpiresAt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -134,8 +143,11 @@ internal sealed class LiveEntityLivenessController
|
|||
|| NonZero(record.Snapshot.WielderId)
|
||||
|| NonZero(record.Snapshot.ParentGuid);
|
||||
_samples.Add(new LiveEntityLivenessSample(
|
||||
record.ProjectionKey
|
||||
?? throw new InvalidOperationException(
|
||||
$"Materialized liveness owner 0x{record.ServerGuid:X8}/" +
|
||||
$"{record.Generation} has no exact projection key."),
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.IsSpatiallyVisible
|
||||
|| IsWithinConservativeVisibility(playerPosition, position),
|
||||
retained));
|
||||
|
|
@ -145,8 +157,8 @@ internal sealed class LiveEntityLivenessController
|
|||
for (int i = 0; i < due.Count; i++)
|
||||
{
|
||||
LiveEntityPruneCandidate candidate = due[i];
|
||||
if (_runtime.TryGetRecord(candidate.ServerGuid, out LiveEntityRecord current)
|
||||
&& current.Generation == candidate.Generation)
|
||||
if (_runtime.TryGetRecord(candidate.Key, out LiveEntityRecord current)
|
||||
&& current.ServerGuid == candidate.ServerGuid)
|
||||
{
|
||||
_prune.Prune(candidate);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.App.Physics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
|
|
@ -28,9 +29,9 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
private readonly Func<(int X, int Y)> _liveCenter;
|
||||
private readonly Action<uint>? _onShadowRestored;
|
||||
private readonly LiveEntityPartArrayEnterWorldPort _partArrayEnterWorld;
|
||||
private readonly Dictionary<uint, ushort> _readyGenerationByGuid = new();
|
||||
private readonly Dictionary<uint, ushort> _suspendedShadowGenerationByGuid = new();
|
||||
private readonly Dictionary<uint, ushort> _activePlacementGenerationByGuid = new();
|
||||
private readonly HashSet<RuntimeEntityKey> _readyOwners = [];
|
||||
private readonly HashSet<RuntimeEntityKey> _suspendedShadowOwners = [];
|
||||
private readonly HashSet<RuntimeEntityKey> _activePlacementOwners = [];
|
||||
private readonly HashSet<LiveEntityRecord> _drainingRecords = new();
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -70,7 +71,8 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
_readyGenerationByGuid[serverGuid] = record.Generation;
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
_readyOwners.Add(key);
|
||||
if (!ApplyPendingTransitions(record)
|
||||
|| record.WorldEntity is not { } entity
|
||||
|| !IsCurrent(record, entity))
|
||||
|
|
@ -91,8 +93,8 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
public bool OnStateAccepted(uint serverGuid)
|
||||
{
|
||||
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
|| !_readyGenerationByGuid.TryGetValue(serverGuid, out ushort generation)
|
||||
|| generation != record.Generation)
|
||||
|| !TryGetProjectionKey(record, out RuntimeEntityKey key)
|
||||
|| !_readyOwners.Contains(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -126,21 +128,13 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
if (_activePlacementGenerationByGuid.TryGetValue(
|
||||
serverGuid,
|
||||
out ushort activeGeneration)
|
||||
&& activeGeneration == generation)
|
||||
{
|
||||
_activePlacementGenerationByGuid.Remove(serverGuid);
|
||||
}
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
_activePlacementOwners.Remove(key);
|
||||
|
||||
if (deferShadowRestore)
|
||||
_suspendedShadowGenerationByGuid[serverGuid] = generation;
|
||||
else if (_suspendedShadowGenerationByGuid.TryGetValue(
|
||||
serverGuid,
|
||||
out ushort deferredGeneration)
|
||||
&& deferredGeneration == generation)
|
||||
_suspendedShadowGenerationByGuid.Remove(serverGuid);
|
||||
_suspendedShadowOwners.Add(key);
|
||||
else
|
||||
_suspendedShadowOwners.Remove(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -158,57 +152,41 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
_activePlacementGenerationByGuid[serverGuid] = generation;
|
||||
if (_suspendedShadowGenerationByGuid.TryGetValue(
|
||||
serverGuid,
|
||||
out ushort deferredGeneration)
|
||||
&& deferredGeneration == generation)
|
||||
{
|
||||
_suspendedShadowGenerationByGuid.Remove(serverGuid);
|
||||
}
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
_activePlacementOwners.Add(key);
|
||||
_suspendedShadowOwners.Remove(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool HasDeferredShadowRestore(uint serverGuid) =>
|
||||
_suspendedShadowGenerationByGuid.ContainsKey(serverGuid);
|
||||
TryGetCurrentProjectionKey(serverGuid, out RuntimeEntityKey key)
|
||||
&& _suspendedShadowOwners.Contains(key);
|
||||
|
||||
internal bool HasActivePlacement(uint serverGuid) =>
|
||||
_activePlacementGenerationByGuid.ContainsKey(serverGuid);
|
||||
TryGetCurrentProjectionKey(serverGuid, out RuntimeEntityKey key)
|
||||
&& _activePlacementOwners.Contains(key);
|
||||
|
||||
internal int ReadyOwnerCount => _readyGenerationByGuid.Count;
|
||||
internal int DeferredShadowRestoreCount => _suspendedShadowGenerationByGuid.Count;
|
||||
internal int ActivePlacementCount => _activePlacementGenerationByGuid.Count;
|
||||
internal int ReadyOwnerCount => _readyOwners.Count;
|
||||
internal int DeferredShadowRestoreCount => _suspendedShadowOwners.Count;
|
||||
internal int ActivePlacementCount => _activePlacementOwners.Count;
|
||||
|
||||
public void Forget(LiveEntityRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (_readyGenerationByGuid.TryGetValue(record.ServerGuid, out ushort generation)
|
||||
&& generation == record.Generation)
|
||||
if (TryGetProjectionKey(record, out RuntimeEntityKey key))
|
||||
{
|
||||
_readyGenerationByGuid.Remove(record.ServerGuid);
|
||||
}
|
||||
if (_suspendedShadowGenerationByGuid.TryGetValue(
|
||||
record.ServerGuid,
|
||||
out ushort suspendedGeneration)
|
||||
&& suspendedGeneration == record.Generation)
|
||||
{
|
||||
_suspendedShadowGenerationByGuid.Remove(record.ServerGuid);
|
||||
}
|
||||
if (_activePlacementGenerationByGuid.TryGetValue(
|
||||
record.ServerGuid,
|
||||
out ushort activeGeneration)
|
||||
&& activeGeneration == record.Generation)
|
||||
{
|
||||
_activePlacementGenerationByGuid.Remove(record.ServerGuid);
|
||||
_readyOwners.Remove(key);
|
||||
_suspendedShadowOwners.Remove(key);
|
||||
_activePlacementOwners.Remove(key);
|
||||
}
|
||||
_drainingRecords.Remove(record);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_readyGenerationByGuid.Clear();
|
||||
_suspendedShadowGenerationByGuid.Clear();
|
||||
_activePlacementGenerationByGuid.Clear();
|
||||
_readyOwners.Clear();
|
||||
_suspendedShadowOwners.Clear();
|
||||
_activePlacementOwners.Clear();
|
||||
_drainingRecords.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +234,7 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
return false;
|
||||
_shadows.Suspend(entity.Id);
|
||||
if (!IsPlacementActive(record))
|
||||
_suspendedShadowGenerationByGuid[record.ServerGuid] = record.Generation;
|
||||
_suspendedShadowOwners.Add(RequireProjectionKey(record));
|
||||
// Retail CPhysicsObj::set_hidden @ 0x00514C60 calls
|
||||
// CPartArray::HandleEnterWorld after hiding the object
|
||||
// from its cell. Despite the name, this is the motion
|
||||
|
|
@ -288,7 +266,7 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
if (restored)
|
||||
_suspendedShadowGenerationByGuid.Remove(record.ServerGuid);
|
||||
_suspendedShadowOwners.Remove(RequireProjectionKey(record));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -345,12 +323,9 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
|
||||
if ((record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0
|
||||
|| IsPlacementActive(record)
|
||||
|| !_readyGenerationByGuid.TryGetValue(record.ServerGuid, out ushort readyGeneration)
|
||||
|| readyGeneration != record.Generation
|
||||
|| !_suspendedShadowGenerationByGuid.TryGetValue(
|
||||
record.ServerGuid,
|
||||
out ushort suspendedGeneration)
|
||||
|| suspendedGeneration != record.Generation)
|
||||
|| !TryGetProjectionKey(record, out RuntimeEntityKey key)
|
||||
|| !_readyOwners.Contains(key)
|
||||
|| !_suspendedShadowOwners.Contains(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -359,7 +334,7 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
if (!IsCurrent(record, entity))
|
||||
return;
|
||||
if (restored)
|
||||
_suspendedShadowGenerationByGuid.Remove(record.ServerGuid);
|
||||
_suspendedShadowOwners.Remove(key);
|
||||
}
|
||||
|
||||
private void SuspendOrdinaryShadowOutsideProjection(
|
||||
|
|
@ -369,16 +344,14 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
if (record.IsSpatiallyVisible
|
||||
|| record.ProjectileRuntime is not null
|
||||
|| IsPlacementActive(record)
|
||||
|| !_readyGenerationByGuid.TryGetValue(
|
||||
record.ServerGuid,
|
||||
out ushort readyGeneration)
|
||||
|| readyGeneration != record.Generation
|
||||
|| !TryGetProjectionKey(record, out RuntimeEntityKey key)
|
||||
|| !_readyOwners.Contains(key)
|
||||
|| !_shadows.Suspend(entity.Id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_suspendedShadowGenerationByGuid[record.ServerGuid] = record.Generation;
|
||||
_suspendedShadowOwners.Add(key);
|
||||
}
|
||||
|
||||
private bool IsCurrent(
|
||||
|
|
@ -390,8 +363,42 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
&& ReferenceEquals(current.WorldEntity, entity);
|
||||
|
||||
private bool IsPlacementActive(LiveEntityRecord record) =>
|
||||
_activePlacementGenerationByGuid.TryGetValue(
|
||||
record.ServerGuid,
|
||||
out ushort generation)
|
||||
&& generation == record.Generation;
|
||||
TryGetProjectionKey(record, out RuntimeEntityKey key)
|
||||
&& _activePlacementOwners.Contains(key);
|
||||
|
||||
private bool TryGetCurrentProjectionKey(
|
||||
uint serverGuid,
|
||||
out RuntimeEntityKey key)
|
||||
{
|
||||
if (_liveEntities.TryGetRecord(
|
||||
serverGuid,
|
||||
out LiveEntityRecord record))
|
||||
{
|
||||
return TryGetProjectionKey(record, out key);
|
||||
}
|
||||
|
||||
key = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetProjectionKey(
|
||||
LiveEntityRecord record,
|
||||
out RuntimeEntityKey key)
|
||||
{
|
||||
if (record.ProjectionKey is { } projectionKey)
|
||||
{
|
||||
key = projectionKey;
|
||||
return true;
|
||||
}
|
||||
|
||||
key = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static RuntimeEntityKey RequireProjectionKey(
|
||||
LiveEntityRecord record) =>
|
||||
record.ProjectionKey
|
||||
?? throw new InvalidOperationException(
|
||||
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
|
||||
"has no exact projection key.");
|
||||
}
|
||||
|
|
|
|||
313
src/AcDream.App/World/LiveEntityProjectionStore.cs
Normal file
313
src/AcDream.App/World/LiveEntityProjectionStore.cs
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
using AcDream.Runtime.Entities;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// App-only projection storage. RuntimeEntityDirectory remains the sole
|
||||
/// server-GUID/incarnation/local-ID authority; every App sidecar exists only
|
||||
/// after Runtime has issued its exact local-ID plus INSTANCE_TS key.
|
||||
/// </summary>
|
||||
internal sealed class LiveEntityProjectionStore
|
||||
{
|
||||
private readonly RuntimeEntityDirectory _directory;
|
||||
private readonly Dictionary<RuntimeEntityKey, LiveEntityRecord> _materialized = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, LiveEntityRecord> _visible = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, LiveEntityRecord> _teardown = new();
|
||||
private readonly Dictionary<RuntimeEntityRecord, LiveEntityRecord>
|
||||
_unmaterializedTeardown = new(ReferenceEqualityComparer.Instance);
|
||||
private readonly List<LiveEntityRecord> _materializedInRegistrationOrder = [];
|
||||
private readonly TeardownRecordCollection _teardownRecords;
|
||||
|
||||
public LiveEntityProjectionStore(RuntimeEntityDirectory directory)
|
||||
{
|
||||
_directory = directory ?? throw new ArgumentNullException(nameof(directory));
|
||||
_teardownRecords = new TeardownRecordCollection(
|
||||
_teardown,
|
||||
_unmaterializedTeardown);
|
||||
}
|
||||
|
||||
public int ActiveCount => _materializedInRegistrationOrder.Count;
|
||||
public int MaterializedCount => _materialized.Count + _teardown.Count;
|
||||
public int TeardownCount => _teardown.Count + _unmaterializedTeardown.Count;
|
||||
public IReadOnlyList<LiveEntityRecord> ActiveRecords =>
|
||||
_materializedInRegistrationOrder;
|
||||
public IReadOnlyList<LiveEntityRecord> Values =>
|
||||
_materializedInRegistrationOrder;
|
||||
public IReadOnlyCollection<LiveEntityRecord> MaterializedRecords =>
|
||||
_materialized.Values;
|
||||
public IReadOnlyCollection<LiveEntityRecord> VisibleRecords => _visible.Values;
|
||||
public IReadOnlyCollection<LiveEntityRecord> TeardownRecords =>
|
||||
_teardownRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the exact App sidecar after Runtime has claimed a local ID.
|
||||
/// Publication is synchronous so no input or presentation callback moves
|
||||
/// to a later frame.
|
||||
/// </summary>
|
||||
public LiveEntityRecord AddMaterializing(RuntimeEntityRecord canonical)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
if (!_directory.IsCurrent(canonical))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"An App projection sidecar can only be added for the current Runtime incarnation.");
|
||||
}
|
||||
RuntimeEntityKey key = canonical.Key
|
||||
?? throw new InvalidOperationException(
|
||||
"Runtime must claim the local projection ID before App creates a sidecar.");
|
||||
if (_materialized.ContainsKey(key))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{canonical.ServerGuid:X8}/{canonical.Incarnation} already has an App sidecar.");
|
||||
}
|
||||
|
||||
var record = new LiveEntityRecord(_directory, canonical)
|
||||
{
|
||||
ProjectionKey = key,
|
||||
};
|
||||
_materialized.Add(key, record);
|
||||
_materializedInRegistrationOrder.Add(record);
|
||||
return record;
|
||||
}
|
||||
|
||||
public bool TryGetCurrent(uint serverGuid, out LiveEntityRecord record)
|
||||
{
|
||||
if (_directory.TryGetActive(serverGuid, out RuntimeEntityRecord canonical))
|
||||
return TryGet(canonical, out record);
|
||||
|
||||
record = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ContainsCurrent(uint serverGuid) =>
|
||||
TryGetCurrent(serverGuid, out _);
|
||||
|
||||
public LiveEntityRecord? GetCurrentOrDefault(uint serverGuid) =>
|
||||
TryGetCurrent(serverGuid, out LiveEntityRecord record) ? record : null;
|
||||
|
||||
public bool TryGet(RuntimeEntityRecord canonical, out LiveEntityRecord record)
|
||||
{
|
||||
if (canonical.Key is { } key
|
||||
&& _materialized.TryGetValue(key, out LiveEntityRecord? candidate)
|
||||
&& ReferenceEquals(candidate.Canonical, canonical))
|
||||
{
|
||||
record = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
record = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGet(RuntimeEntityKey key, out LiveEntityRecord record) =>
|
||||
_materialized.TryGetValue(key, out record!);
|
||||
|
||||
public bool TryGetByLocalId(uint localEntityId, out LiveEntityRecord record)
|
||||
{
|
||||
if (_directory.TryGetByLocalId(
|
||||
localEntityId,
|
||||
out RuntimeEntityRecord canonical))
|
||||
{
|
||||
return TryGet(canonical, out record);
|
||||
}
|
||||
|
||||
record = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsCurrent(LiveEntityRecord record) =>
|
||||
_directory.IsCurrent(record.Canonical)
|
||||
&& TryGet(record.Canonical, out LiveEntityRecord current)
|
||||
&& ReferenceEquals(current, record);
|
||||
|
||||
public void SetVisible(LiveEntityRecord record, bool visible)
|
||||
{
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
if (!_materialized.TryGetValue(key, out LiveEntityRecord? materialized)
|
||||
|| !ReferenceEquals(materialized, record))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Only a current exact App sidecar can change visibility.");
|
||||
}
|
||||
|
||||
if (visible)
|
||||
_visible[key] = record;
|
||||
else
|
||||
_visible.Remove(key);
|
||||
}
|
||||
|
||||
public bool TryGetVisibleCurrent(uint serverGuid, out LiveEntityRecord record)
|
||||
{
|
||||
if (_directory.TryGetActive(serverGuid, out RuntimeEntityRecord canonical)
|
||||
&& canonical.Key is { } key
|
||||
&& _visible.TryGetValue(key, out LiveEntityRecord? visible)
|
||||
&& ReferenceEquals(visible.Canonical, canonical))
|
||||
{
|
||||
record = visible;
|
||||
return true;
|
||||
}
|
||||
|
||||
record = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool RemoveCurrent(
|
||||
uint serverGuid,
|
||||
out LiveEntityRecord? record)
|
||||
{
|
||||
if (!_directory.TryGetActive(
|
||||
serverGuid,
|
||||
out RuntimeEntityRecord canonical)
|
||||
|| !TryGet(canonical, out LiveEntityRecord sidecar)
|
||||
|| !RemoveActive(sidecar))
|
||||
{
|
||||
record = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
record = sidecar;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveCurrent(uint serverGuid) =>
|
||||
RemoveCurrent(serverGuid, out _);
|
||||
|
||||
public bool RemoveActive(LiveEntityRecord record)
|
||||
{
|
||||
if (record.ProjectionKey is not { } key)
|
||||
return false;
|
||||
|
||||
_visible.Remove(key);
|
||||
bool removed = _materialized.Remove(
|
||||
key,
|
||||
out LiveEntityRecord? materialized)
|
||||
&& ReferenceEquals(materialized, record);
|
||||
if (!removed)
|
||||
return false;
|
||||
if (!_materializedInRegistrationOrder.Remove(record))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The exact App sidecar was absent from materialization order.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RetainTeardown(LiveEntityRecord record)
|
||||
{
|
||||
if (record.ProjectionKey is { } key)
|
||||
AddExact(_teardown, key, record);
|
||||
else
|
||||
AddExact(_unmaterializedTeardown, record.Canonical, record);
|
||||
}
|
||||
|
||||
public bool TryGetTeardown(
|
||||
RuntimeEntityRecord canonical,
|
||||
out LiveEntityRecord record)
|
||||
{
|
||||
if (canonical.Key is { } key
|
||||
&& _teardown.TryGetValue(key, out record!))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
foreach (LiveEntityRecord retained in _teardown.Values)
|
||||
{
|
||||
if (ReferenceEquals(retained.Canonical, canonical))
|
||||
{
|
||||
record = retained;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return _unmaterializedTeardown.TryGetValue(canonical, out record!);
|
||||
}
|
||||
|
||||
public void ReleaseTeardown(LiveEntityRecord record)
|
||||
{
|
||||
if (record.ProjectionKey is { } key)
|
||||
{
|
||||
RemoveExact(_teardown, key, record);
|
||||
record.ProjectionKey = null;
|
||||
return;
|
||||
}
|
||||
|
||||
RemoveExact(_unmaterializedTeardown, record.Canonical, record);
|
||||
}
|
||||
|
||||
public bool IsRetainedTeardown(LiveEntityRecord record) =>
|
||||
TryGetTeardown(record.Canonical, out LiveEntityRecord retained)
|
||||
&& ReferenceEquals(retained, record);
|
||||
|
||||
public void ClearConverged()
|
||||
{
|
||||
if (_materializedInRegistrationOrder.Count != 0 || TeardownCount != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Projection storage cannot clear before active and teardown ownership converges.");
|
||||
}
|
||||
|
||||
_materialized.Clear();
|
||||
_visible.Clear();
|
||||
_teardown.Clear();
|
||||
_unmaterializedTeardown.Clear();
|
||||
}
|
||||
|
||||
private static RuntimeEntityKey RequireProjectionKey(LiveEntityRecord record) =>
|
||||
record.ProjectionKey
|
||||
?? throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Generation} has no App projection key.");
|
||||
|
||||
private static void AddExact<TKey>(
|
||||
Dictionary<TKey, LiveEntityRecord> destination,
|
||||
TKey key,
|
||||
LiveEntityRecord record)
|
||||
where TKey : notnull
|
||||
{
|
||||
if (destination.TryGetValue(key, out LiveEntityRecord? retained)
|
||||
&& !ReferenceEquals(retained, record))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"App teardown sidecar collision for 0x{record.ServerGuid:X8}/{record.Generation}.");
|
||||
}
|
||||
destination[key] = record;
|
||||
}
|
||||
|
||||
private static void RemoveExact<TKey>(
|
||||
Dictionary<TKey, LiveEntityRecord> source,
|
||||
TKey key,
|
||||
LiveEntityRecord record)
|
||||
where TKey : notnull
|
||||
{
|
||||
if (source.TryGetValue(key, out LiveEntityRecord? retained)
|
||||
&& ReferenceEquals(retained, record))
|
||||
{
|
||||
source.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TeardownRecordCollection :
|
||||
IReadOnlyCollection<LiveEntityRecord>
|
||||
{
|
||||
private readonly Dictionary<RuntimeEntityKey, LiveEntityRecord> _keyed;
|
||||
private readonly Dictionary<RuntimeEntityRecord, LiveEntityRecord> _unmaterialized;
|
||||
|
||||
public TeardownRecordCollection(
|
||||
Dictionary<RuntimeEntityKey, LiveEntityRecord> keyed,
|
||||
Dictionary<RuntimeEntityRecord, LiveEntityRecord> unmaterialized)
|
||||
{
|
||||
_keyed = keyed;
|
||||
_unmaterialized = unmaterialized;
|
||||
}
|
||||
|
||||
public int Count => _keyed.Count + _unmaterialized.Count;
|
||||
|
||||
public IEnumerator<LiveEntityRecord> GetEnumerator()
|
||||
{
|
||||
foreach (LiveEntityRecord record in _keyed.Values)
|
||||
yield return record;
|
||||
foreach (LiveEntityRecord record in _unmaterialized.Values)
|
||||
yield return record;
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() =>
|
||||
GetEnumerator();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -106,11 +106,6 @@ internal sealed class LiveEntityRuntimeTeardownController
|
|||
{
|
||||
LiveEntityRuntime runtime = _runtime!;
|
||||
uint serverGuid = record.ServerGuid;
|
||||
var incarnation = new LiveEntityIncarnationCleanup(
|
||||
record,
|
||||
guid => runtime.TryGetRecord(guid, out LiveEntityRecord current)
|
||||
? current
|
||||
: null);
|
||||
var cleanups = new List<Action>
|
||||
{
|
||||
() => _presentation!.Forget(record),
|
||||
|
|
@ -152,17 +147,17 @@ internal sealed class LiveEntityRuntimeTeardownController
|
|||
cleanups.Add(physicsHost.NotifyExitWorld);
|
||||
}
|
||||
|
||||
cleanups.Add(() => _animations.Remove(existingEntity.Id));
|
||||
cleanups.Add(() => _animations.Remove(record));
|
||||
cleanups.Add(() => _classification!.InvalidateEntity(existingEntity.Id));
|
||||
cleanups.Add(() => incarnation.RunIfNoReplacement(
|
||||
() => _remoteMovementObservations!.Remove(serverGuid)));
|
||||
if (record.ProjectionKey is { } projectionKey)
|
||||
cleanups.Add(() => _remoteMovementObservations!.Remove(projectionKey));
|
||||
cleanups.Add(() => _translucencyFades!.ClearEntity(existingEntity.Id));
|
||||
cleanups.Add(() => _projectionWithdrawal!.LeaveWorld(
|
||||
record,
|
||||
_identity!.ServerGuid));
|
||||
cleanups.Add(() => _children!.OnLogicalTeardown(record));
|
||||
cleanups.Add(() => _shadows!.Deregister(existingEntity.Id));
|
||||
cleanups.Add(() => _lights!.Forget(existingEntity.Id));
|
||||
cleanups.Add(() => _lights!.Forget(record));
|
||||
return new LiveEntityTeardownPlan(cleanups);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
|||
internal LiveEntityAnimationRuntimeView(ILiveEntityRuntimeSource runtime) =>
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
|
||||
public int Count => _runtime.Current?.SpatialAnimationRuntimes.Count ?? 0;
|
||||
public IEnumerable<uint> Keys =>
|
||||
_runtime.Current?.SpatialAnimationRuntimes.Keys ?? Array.Empty<uint>();
|
||||
public int Count => _runtime.Current?.SpatialAnimationRuntimeCount ?? 0;
|
||||
|
||||
public TAnimation this[uint localEntityId]
|
||||
{
|
||||
|
|
@ -52,6 +50,12 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
|||
&& runtime.ClearAnimationRuntime(guid);
|
||||
}
|
||||
|
||||
internal bool Remove(LiveEntityRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
return _runtime.Current?.ClearAnimationRuntime(record) == true;
|
||||
}
|
||||
|
||||
/// <summary>Copies only currently resident local IDs without boxing a
|
||||
/// dictionary-key enumerator.</summary>
|
||||
public void CopySpatialIdsTo(HashSet<uint> destination)
|
||||
|
|
@ -110,10 +114,9 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
|||
while (++_index < _snapshot.Count)
|
||||
{
|
||||
KeyValuePair<uint, TAnimation> pair = _snapshot[_index];
|
||||
if (_runtime?.SpatialAnimationRuntimes.TryGetValue(
|
||||
if (_runtime?.IsCurrentSpatialAnimation(
|
||||
pair.Key,
|
||||
out ILiveEntityAnimationRuntime? indexed) == true
|
||||
&& ReferenceEquals(indexed, pair.Value))
|
||||
pair.Value) == true)
|
||||
{
|
||||
Current = pair;
|
||||
return true;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue