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
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue