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