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:
Erik 2026-07-25 21:50:58 +02:00
parent e18df84437
commit 420e5eea70
73 changed files with 2939 additions and 1715 deletions

View file

@ -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.");
}