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

@ -4,6 +4,7 @@ using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Vfx;
using AcDream.Core.World;
using AcDream.Runtime.Entities;
using DatReaderWriter.Types;
namespace AcDream.App.Rendering.Vfx;
@ -33,16 +34,17 @@ public sealed class EntityEffectController : IAnimationHookSink,
private readonly Func<uint, uint?> _parentOfAttachedChild;
private readonly Action<uint> _ownerUnregistered;
private readonly Action<uint, uint?> _ownerSoundTableChanged;
private readonly Dictionary<uint, EntityEffectProfile> _profilesByLocalId = new();
private readonly Dictionary<uint, ushort> _readyGenerationByServerGuid = new();
private readonly Dictionary<RuntimeEntityKey, EntityEffectProfile> _liveProfiles = [];
private readonly HashSet<RuntimeEntityKey> _readyLiveOwners = [];
private readonly Dictionary<uint, Queue<PendingEffect>> _pendingByServerGuid = new();
private readonly Dictionary<uint, WorldEntity> _staticOwners = new();
private readonly Dictionary<uint, EntityEffectProfile> _staticProfiles = new();
private readonly HashSet<uint> _syntheticOwners = new();
private readonly HashSet<LiveEntityRecord> _dirtyLiveOwners =
new(ReferenceEqualityComparer.Instance);
private readonly List<LiveEntityRecord> _dirtyLiveOwnerOrder = [];
private readonly List<LiveEntityRecord> _dirtyLiveOwnerSnapshot = [];
private readonly List<uint> _readyGuidSnapshot = [];
private readonly List<RuntimeEntityKey> _readyKeySnapshot = [];
private uint _posePublishLocalId;
private Action<string>? _diagnosticSink = Console.WriteLine;
@ -76,7 +78,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
}
public int PendingPacketCount => _pendingByServerGuid.Values.Sum(queue => queue.Count);
public int ReadyOwnerCount => _profilesByLocalId.Count;
public int ReadyOwnerCount => _liveProfiles.Count + _staticProfiles.Count;
internal int LastPoseRefreshOwnerVisitCount { get; private set; }
public void HandleDirect(PlayPhysicsScript message)
@ -135,8 +137,9 @@ public sealed class EntityEffectController : IAnimationHookSink,
return false;
}
_readyGenerationByServerGuid[serverGuid] = record.Generation;
_profilesByLocalId[entity.Id] = profile;
RuntimeEntityKey key = RequireProjectionKey(record);
_readyLiveOwners.Add(key);
_liveProfiles[key] = profile;
_runner.SetOwnerAnchor(entity.Id, entity.Position);
_ownerSoundTableChanged(entity.Id, profile.CurrentSoundTableDid);
return true;
@ -166,7 +169,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
return false;
}
_profilesByLocalId[localId] = profile;
_liveProfiles[RequireProjectionKey(record)] = profile;
_ownerSoundTableChanged(localId, profile.CurrentSoundTableDid);
return true;
}
@ -181,7 +184,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
if (ownerLocalId == 0)
return;
_staticOwners[ownerLocalId] = entity;
_profilesByLocalId[ownerLocalId] = profile;
_staticProfiles[ownerLocalId] = profile;
_runner.SetOwnerAnchor(ownerLocalId, entity.Position);
_ownerSoundTableChanged(ownerLocalId, profile.CurrentSoundTableDid);
}
@ -190,7 +193,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
{
if (!_staticOwners.Remove(localId))
return;
_profilesByLocalId.Remove(localId);
_staticProfiles.Remove(localId);
_runner.StopAllForEntity(localId);
_ownerUnregistered(localId);
}
@ -212,16 +215,13 @@ public sealed class EntityEffectController : IAnimationHookSink,
{
_pendingByServerGuid.Remove(record.ServerGuid);
}
if (_readyGenerationByServerGuid.TryGetValue(
record.ServerGuid,
out ushort readyGeneration)
&& readyGeneration == record.Generation)
if (record.ProjectionKey is { } key)
{
_readyGenerationByServerGuid.Remove(record.ServerGuid);
_readyLiveOwners.Remove(key);
_liveProfiles.Remove(key);
}
if (record.LocalEntityId is not { } localId)
return;
_profilesByLocalId.Remove(localId);
_runner.StopAllForEntity(localId);
_ownerUnregistered(localId);
}
@ -232,18 +232,15 @@ public sealed class EntityEffectController : IAnimationHookSink,
public void ClearNetworkState()
{
_readyGuidSnapshot.Clear();
_readyGuidSnapshot.AddRange(_readyGenerationByServerGuid.Keys);
foreach (uint serverGuid in _readyGuidSnapshot)
_readyKeySnapshot.Clear();
_readyKeySnapshot.AddRange(_readyLiveOwners);
foreach (RuntimeEntityKey key in _readyKeySnapshot)
{
if (_liveEntities.TryGetLocalEntityId(serverGuid, out uint localId))
{
_profilesByLocalId.Remove(localId);
_runner.StopAllForEntity(localId);
_ownerUnregistered(localId);
}
_runner.StopAllForEntity(key.LocalEntityId);
_ownerUnregistered(key.LocalEntityId);
}
_readyGenerationByServerGuid.Clear();
_readyLiveOwners.Clear();
_liveProfiles.Clear();
_pendingByServerGuid.Clear();
_dirtyLiveOwners.Clear();
_dirtyLiveOwnerOrder.Clear();
@ -331,7 +328,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
uint rawScriptType,
float intensity)
{
if (!_profilesByLocalId.TryGetValue(ownerLocalId, out EntityEffectProfile? profile)
if (!TryGetProfile(ownerLocalId, out EntityEffectProfile? profile)
|| profile.CurrentPhysicsScriptTableDid is not { } tableDid)
{
DiagnosticSink?.Invoke(
@ -360,7 +357,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
public bool PlayDefault(uint ownerLocalId)
{
if (!CanStartOwner(ownerLocalId)
|| !_profilesByLocalId.TryGetValue(ownerLocalId, out EntityEffectProfile? profile))
|| !TryGetProfile(ownerLocalId, out EntityEffectProfile? profile))
return false;
return PlayTyped(
ownerLocalId,
@ -433,8 +430,9 @@ public sealed class EntityEffectController : IAnimationHookSink,
private bool TryGetLiveRoot(uint ownerLocalId, out LiveEntityRecord record)
{
if (_liveEntities.TryGetServerGuid(ownerLocalId, out uint serverGuid)
&& _liveEntities.TryGetRecord(serverGuid, out record!))
if (_liveEntities.TryGetRecordByLocalEntityId(
ownerLocalId,
out record!))
{
return true;
}
@ -444,12 +442,12 @@ public sealed class EntityEffectController : IAnimationHookSink,
private bool TryGetReadyLocalId(uint serverGuid, out uint localId)
{
if (_readyGenerationByServerGuid.TryGetValue(serverGuid, out ushort readyGeneration)
&& _liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
&& record.Generation == readyGeneration
&& _liveEntities.TryGetLocalEntityId(serverGuid, out localId)
&& _profilesByLocalId.ContainsKey(localId))
if (_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
&& record.ProjectionKey is { } key
&& _readyLiveOwners.Contains(key)
&& _liveProfiles.ContainsKey(key))
{
localId = key.LocalEntityId;
return true;
}
@ -461,8 +459,9 @@ public sealed class EntityEffectController : IAnimationHookSink,
{
if (_posePublishLocalId == localId)
return;
if (_liveEntities.TryGetServerGuid(localId, out uint serverGuid)
&& _liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record))
if (_liveEntities.TryGetRecordByLocalEntityId(
localId,
out LiveEntityRecord record))
{
MarkLiveOwnerPoseDirty(record);
}
@ -476,10 +475,8 @@ public sealed class EntityEffectController : IAnimationHookSink,
private void MarkLiveOwnerPoseDirty(LiveEntityRecord record)
{
if (!_readyGenerationByServerGuid.TryGetValue(
record.ServerGuid,
out ushort readyGeneration)
|| readyGeneration != record.Generation
if (record.ProjectionKey is not { } key
|| !_readyLiveOwners.Contains(key)
|| !_dirtyLiveOwners.Add(record))
{
return;
@ -550,6 +547,29 @@ public sealed class EntityEffectController : IAnimationHookSink,
PlayTyped(localId, effect.RawScriptType, effect.Intensity);
}
private bool TryGetProfile(
uint ownerLocalId,
out EntityEffectProfile profile)
{
if (_liveEntities.TryGetRecordByLocalEntityId(
ownerLocalId,
out LiveEntityRecord record)
&& record.ProjectionKey is { } key
&& _liveProfiles.TryGetValue(key, out profile!))
{
return true;
}
return _staticProfiles.TryGetValue(ownerLocalId, out profile!);
}
private static RuntimeEntityKey RequireProjectionKey(
LiveEntityRecord record) =>
record.ProjectionKey
?? throw new InvalidOperationException(
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
"has no exact projection key.");
private enum PendingEffectKind
{
Direct,

View file

@ -1,6 +1,7 @@
using AcDream.App.World;
using AcDream.Core.Lighting;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Rendering.Vfx;
@ -22,8 +23,8 @@ public sealed class LiveEntityLightController : IDisposable
private readonly EntityEffectPoseRegistry _poses;
private readonly LightingHookSink _lighting;
private readonly Func<uint, Setup?> _loadSetup;
private readonly Dictionary<uint, uint> _serverGuidByOwner = new();
private readonly HashSet<uint> _presentOwners = new();
private readonly HashSet<RuntimeEntityKey> _trackedOwners = [];
private readonly HashSet<RuntimeEntityKey> _presentOwners = [];
public LiveEntityLightController(
LiveEntityRuntime liveEntities,
@ -49,8 +50,9 @@ public sealed class LiveEntityLightController : IDisposable
return false;
}
_serverGuidByOwner[entity.Id] = serverGuid;
_presentOwners.Add(entity.Id);
RuntimeEntityKey key = RequireProjectionKey(record);
_trackedOwners.Add(key);
_presentOwners.Add(key);
_lighting.UnregisterOwner(entity.Id, forgetState: false);
_lighting.InitializeOwnerLighting(
entity.Id,
@ -87,7 +89,13 @@ public sealed class LiveEntityLightController : IDisposable
/// <summary>Withdraw cell-scoped presentation but retain logical light state.</summary>
public void Unregister(uint ownerLocalId)
{
_presentOwners.Remove(ownerLocalId);
if (_liveEntities.TryGetRecordByLocalEntityId(
ownerLocalId,
out LiveEntityRecord record)
&& record.ProjectionKey is { } key)
{
_presentOwners.Remove(key);
}
_lighting.UnregisterOwner(ownerLocalId, forgetState: false);
}
@ -105,16 +113,20 @@ public sealed class LiveEntityLightController : IDisposable
}
/// <summary>End logical ownership after leave-world presentation teardown.</summary>
public void Forget(uint ownerLocalId)
public void Forget(LiveEntityRecord record)
{
_presentOwners.Remove(ownerLocalId);
_serverGuidByOwner.Remove(ownerLocalId);
_lighting.UnregisterOwner(ownerLocalId);
ArgumentNullException.ThrowIfNull(record);
if (record.ProjectionKey is not { } key)
return;
_presentOwners.Remove(key);
_trackedOwners.Remove(key);
_lighting.UnregisterOwner(key.LocalEntityId);
}
public void Refresh() => _lighting.RefreshAttachedLights();
internal int TrackedOwnerCount => _serverGuidByOwner.Count;
internal int TrackedOwnerCount => _trackedOwners.Count;
internal int PresentedOwnerCount => _presentOwners.Count;
/// <summary>
@ -127,7 +139,8 @@ public sealed class LiveEntityLightController : IDisposable
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|| record.ProjectionKind is not LiveEntityProjectionKind.Attached
|| record.WorldEntity is not { } entity
|| _presentOwners.Contains(entity.Id))
|| record.ProjectionKey is not { } key
|| _presentOwners.Contains(key))
{
return;
}
@ -136,8 +149,11 @@ public sealed class LiveEntityLightController : IDisposable
private void OnOwnerLightingChanged(uint ownerLocalId, bool enabled)
{
if (!_presentOwners.Contains(ownerLocalId)
|| !_serverGuidByOwner.TryGetValue(ownerLocalId, out uint serverGuid))
if (!_liveEntities.TryGetRecordByLocalEntityId(
ownerLocalId,
out LiveEntityRecord record)
|| record.ProjectionKey is not { } key
|| !_presentOwners.Contains(key))
{
return;
}
@ -148,15 +164,17 @@ public sealed class LiveEntityLightController : IDisposable
return;
}
Register(serverGuid);
Register(record.ServerGuid);
}
public void Dispose()
{
_lighting.OwnerLightingChanged -= OnOwnerLightingChanged;
_liveEntities.ProjectionVisibilityChanged -= OnProjectionVisibilityChanged;
foreach (uint ownerId in _serverGuidByOwner.Keys.ToArray())
Forget(ownerId);
foreach (RuntimeEntityKey key in _trackedOwners)
_lighting.UnregisterOwner(key.LocalEntityId);
_trackedOwners.Clear();
_presentOwners.Clear();
}
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
@ -176,4 +194,11 @@ public sealed class LiveEntityLightController : IDisposable
else
Unregister(entity.Id);
}
private static RuntimeEntityKey RequireProjectionKey(
LiveEntityRecord record) =>
record.ProjectionKey
?? throw new InvalidOperationException(
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
"has no exact projection key.");
}