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,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);
}