fix(streaming): preserve exact live projection identity
Carry RuntimeEntityKey through spatial residency, visibility transitions, rebucketing, quiescence, landblock retirement, and origin recentering. This prevents stale incarnation edges from mutating a replacement projection while retaining the static DAT-entity path. Validated by 104 focused ownership/streaming tests, the Release solution build, and 8,444 complete Release tests with five existing skips. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
420e5eea70
commit
e937cc36df
8 changed files with 338 additions and 120 deletions
|
|
@ -335,10 +335,10 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
live.WorldAvailability,
|
live.WorldAvailability,
|
||||||
d.Selection,
|
d.Selection,
|
||||||
live.WorldState,
|
live.WorldState,
|
||||||
guid => live.LiveEntities.TryGetLocalEntityId(
|
guid => live.LiveEntities.TryGetProjectionKey(
|
||||||
guid,
|
guid,
|
||||||
out uint localEntityId)
|
out AcDream.Runtime.Entities.RuntimeEntityKey key)
|
||||||
? localEntityId
|
? key
|
||||||
: null,
|
: null,
|
||||||
content.Audio?.Engine);
|
content.Audio?.Engine);
|
||||||
var compositeWarmupSource =
|
var compositeWarmupSource =
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.App.Streaming;
|
namespace AcDream.App.Streaming;
|
||||||
|
|
||||||
|
|
@ -150,13 +151,13 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
// This index mirrors only loaded live projections and changes at the same
|
// This index mirrors only loaded live projections and changes at the same
|
||||||
// transaction boundary as _projectionLocations.
|
// transaction boundary as _projectionLocations.
|
||||||
private readonly Dictionary<uint, HashSet<WorldEntity>> _loadedLiveByLandblock = new();
|
private readonly Dictionary<uint, HashSet<WorldEntity>> _loadedLiveByLandblock = new();
|
||||||
// Runtime-issued local IDs stay unique for the complete materialized
|
// RuntimeEntityKey is the complete materialized identity. Local IDs remain
|
||||||
// lifetime, including retryable teardown. Spatial storage therefore never
|
// stable through retryable teardown, while the incarnation prevents a
|
||||||
// groups graphical owners by reusable server GUID.
|
// delayed spatial edge from reaching a later reuse of the same local ID.
|
||||||
private readonly Dictionary<uint, WorldEntity> _liveProjectionByLocalId = new();
|
private readonly Dictionary<RuntimeEntityKey, WorldEntity> _liveProjectionByKey = new();
|
||||||
private readonly Dictionary<uint, int> _visibleLiveProjectionCounts = new();
|
private readonly Dictionary<RuntimeEntityKey, int> _visibleLiveProjectionCounts = new();
|
||||||
private readonly Dictionary<uint, bool> _visibilityBeforeMutation = new();
|
private readonly Dictionary<RuntimeEntityKey, bool> _visibilityBeforeMutation = new();
|
||||||
private readonly Queue<(uint LocalEntityId, bool Visible)> _visibilityTransitions = new();
|
private readonly Queue<(RuntimeEntityKey Key, bool Visible)> _visibilityTransitions = new();
|
||||||
private readonly List<WorldEntity> _guidRemovalScratch = new();
|
private readonly List<WorldEntity> _guidRemovalScratch = new();
|
||||||
private bool _dispatchingVisibilityTransitions;
|
private bool _dispatchingVisibilityTransitions;
|
||||||
private int _mutationDepth;
|
private int _mutationDepth;
|
||||||
|
|
@ -167,7 +168,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
public IReadOnlyList<WorldEntity> Entities => _flatEntities;
|
public IReadOnlyList<WorldEntity> Entities => _flatEntities;
|
||||||
public ulong FlatViewGeneration => _flatViewGeneration;
|
public ulong FlatViewGeneration => _flatViewGeneration;
|
||||||
public IReadOnlyCollection<uint> LoadedLandblockIds => _loaded.Keys;
|
public IReadOnlyCollection<uint> LoadedLandblockIds => _loaded.Keys;
|
||||||
public event Action<uint, bool>? LiveProjectionVisibilityChanged;
|
public event Action<RuntimeEntityKey, bool>? LiveProjectionVisibilityChanged;
|
||||||
|
|
||||||
public bool IsLoaded(uint landblockId) => _loaded.ContainsKey(landblockId);
|
public bool IsLoaded(uint landblockId) => _loaded.ContainsKey(landblockId);
|
||||||
public bool IsNearTier(uint landblockId) =>
|
public bool IsNearTier(uint landblockId) =>
|
||||||
|
|
@ -184,13 +185,13 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
/// presentation gate: destination objects must acquire their render
|
/// presentation gate: destination objects must acquire their render
|
||||||
/// resources while portal/login reveal keeps normal world consumers closed.
|
/// resources while portal/login reveal keeps normal world consumers closed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsLiveEntityProjectionResident(uint localEntityId) =>
|
public bool IsLiveEntityProjectionResident(RuntimeEntityKey key) =>
|
||||||
localEntityId != 0
|
key.LocalEntityId != 0
|
||||||
&& _visibleLiveProjectionCounts.ContainsKey(localEntityId);
|
&& _visibleLiveProjectionCounts.ContainsKey(key);
|
||||||
|
|
||||||
public bool IsLiveEntityVisible(uint localEntityId) =>
|
public bool IsLiveEntityVisible(RuntimeEntityKey key) =>
|
||||||
_availability.IsWorldAvailable
|
_availability.IsWorldAvailable
|
||||||
&& IsLiveEntityProjectionResident(localEntityId);
|
&& IsLiveEntityProjectionResident(key);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try to grab the loaded record for a landblock — useful for callers
|
/// Try to grab the loaded record for a landblock — useful for callers
|
||||||
|
|
@ -573,7 +574,8 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
private readonly record struct ProjectionLocation(
|
private readonly record struct ProjectionLocation(
|
||||||
uint LandblockId,
|
uint LandblockId,
|
||||||
bool IsLoaded,
|
bool IsLoaded,
|
||||||
int BucketIndex);
|
int BucketIndex,
|
||||||
|
RuntimeEntityKey Key);
|
||||||
|
|
||||||
private void EndMutationBatch()
|
private void EndMutationBatch()
|
||||||
{
|
{
|
||||||
|
|
@ -582,11 +584,11 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
if (--_mutationDepth != 0)
|
if (--_mutationDepth != 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach ((uint localEntityId, bool wasVisible) in _visibilityBeforeMutation)
|
foreach ((RuntimeEntityKey key, bool wasVisible) in _visibilityBeforeMutation)
|
||||||
{
|
{
|
||||||
bool isVisible = _visibleLiveProjectionCounts.ContainsKey(localEntityId);
|
bool isVisible = _visibleLiveProjectionCounts.ContainsKey(key);
|
||||||
if (wasVisible != isVisible)
|
if (wasVisible != isVisible)
|
||||||
_visibilityTransitions.Enqueue((localEntityId, isVisible));
|
_visibilityTransitions.Enqueue((key, isVisible));
|
||||||
}
|
}
|
||||||
_visibilityBeforeMutation.Clear();
|
_visibilityBeforeMutation.Clear();
|
||||||
_visibilityCommitCount++;
|
_visibilityCommitCount++;
|
||||||
|
|
@ -601,29 +603,35 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
ProbeFlatViewTransitions();
|
ProbeFlatViewTransitions();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AdjustVisibleLiveProjection(WorldEntity entity, int delta)
|
private void AdjustVisibleLiveProjection(
|
||||||
|
RuntimeEntityKey key,
|
||||||
|
WorldEntity entity,
|
||||||
|
int delta)
|
||||||
{
|
{
|
||||||
uint localEntityId = entity.Id;
|
if (entity.ServerGuid == 0 || key.LocalEntityId == 0 || delta == 0)
|
||||||
if (entity.ServerGuid == 0 || localEntityId == 0 || delta == 0)
|
|
||||||
return;
|
return;
|
||||||
|
if (key.LocalEntityId != entity.Id)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"A live visibility key must match its WorldEntity local ID.");
|
||||||
|
|
||||||
_visibleLiveProjectionCounts.TryGetValue(localEntityId, out int priorCount);
|
_visibleLiveProjectionCounts.TryGetValue(key, out int priorCount);
|
||||||
if (!_visibilityBeforeMutation.ContainsKey(localEntityId))
|
if (!_visibilityBeforeMutation.ContainsKey(key))
|
||||||
_visibilityBeforeMutation.Add(localEntityId, priorCount > 0);
|
_visibilityBeforeMutation.Add(key, priorCount > 0);
|
||||||
|
|
||||||
int nextCount = checked(priorCount + delta);
|
int nextCount = checked(priorCount + delta);
|
||||||
if (nextCount < 0)
|
if (nextCount < 0)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Live projection visibility count underflow for local " +
|
$"Live projection visibility count underflow for local " +
|
||||||
$"0x{localEntityId:X8} / server 0x{entity.ServerGuid:X8}.");
|
$"0x{key.LocalEntityId:X8}/{key.Incarnation} / " +
|
||||||
|
$"server 0x{entity.ServerGuid:X8}.");
|
||||||
|
|
||||||
if (nextCount == 0)
|
if (nextCount == 0)
|
||||||
{
|
{
|
||||||
_visibleLiveProjectionCounts.Remove(localEntityId);
|
_visibleLiveProjectionCounts.Remove(key);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_visibleLiveProjectionCounts[localEntityId] = nextCount;
|
_visibleLiveProjectionCounts[key] = nextCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -669,7 +677,8 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
WorldEntity entity,
|
WorldEntity entity,
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
bool isLoaded,
|
bool isLoaded,
|
||||||
int bucketIndex)
|
int bucketIndex,
|
||||||
|
RuntimeEntityKey? requestedKey = null)
|
||||||
{
|
{
|
||||||
if (entity.ServerGuid == 0)
|
if (entity.ServerGuid == 0)
|
||||||
return;
|
return;
|
||||||
|
|
@ -677,6 +686,19 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
bool isNew = !_projectionLocations.TryGetValue(
|
bool isNew = !_projectionLocations.TryGetValue(
|
||||||
entity,
|
entity,
|
||||||
out ProjectionLocation priorLocation);
|
out ProjectionLocation priorLocation);
|
||||||
|
RuntimeEntityKey key = isNew
|
||||||
|
? requestedKey ?? new RuntimeEntityKey(entity.Id, 0)
|
||||||
|
: priorLocation.Key;
|
||||||
|
if (key.LocalEntityId == 0 || key.LocalEntityId != entity.Id)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The exact spatial projection key must match the WorldEntity local ID.");
|
||||||
|
}
|
||||||
|
if (!isNew && requestedKey is { } requested && requested != key)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"A live spatial projection cannot change exact Runtime identity.");
|
||||||
|
}
|
||||||
if (!isNew
|
if (!isNew
|
||||||
&& priorLocation.IsLoaded
|
&& priorLocation.IsLoaded
|
||||||
&& (!isLoaded || priorLocation.LandblockId != landblockId))
|
&& (!isLoaded || priorLocation.LandblockId != landblockId))
|
||||||
|
|
@ -686,7 +708,8 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
_projectionLocations[entity] = new ProjectionLocation(
|
_projectionLocations[entity] = new ProjectionLocation(
|
||||||
landblockId,
|
landblockId,
|
||||||
isLoaded,
|
isLoaded,
|
||||||
bucketIndex);
|
bucketIndex,
|
||||||
|
key);
|
||||||
if (isLoaded
|
if (isLoaded
|
||||||
&& (isNew || !priorLocation.IsLoaded || priorLocation.LandblockId != landblockId))
|
&& (isNew || !priorLocation.IsLoaded || priorLocation.LandblockId != landblockId))
|
||||||
{
|
{
|
||||||
|
|
@ -695,10 +718,10 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
if (!isNew)
|
if (!isNew)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_liveProjectionByLocalId.TryAdd(entity.Id, entity))
|
if (!_liveProjectionByKey.TryAdd(key, entity))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Runtime local projection id 0x{entity.Id:X8} is already spatially owned.");
|
$"Runtime projection {key} is already spatially owned.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -710,17 +733,17 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
if (location.IsLoaded)
|
if (location.IsLoaded)
|
||||||
RemoveLoadedLiveIndex(location.LandblockId, entity);
|
RemoveLoadedLiveIndex(location.LandblockId, entity);
|
||||||
|
|
||||||
if (!_liveProjectionByLocalId.Remove(
|
if (!_liveProjectionByKey.Remove(
|
||||||
entity.Id,
|
location.Key,
|
||||||
out WorldEntity? indexed))
|
out WorldEntity? indexed))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Live projection 0x{entity.Id:X8} had no exact local-id index entry.");
|
$"Live projection {location.Key} had no exact-key index entry.");
|
||||||
}
|
}
|
||||||
if (!ReferenceEquals(indexed, entity))
|
if (!ReferenceEquals(indexed, entity))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Runtime local projection id 0x{entity.Id:X8} resolved a different owner.");
|
$"Runtime projection {location.Key} resolved a different owner.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -754,7 +777,10 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
_loadedLiveByLandblock.Remove(landblockId);
|
_loadedLiveByLandblock.Remove(landblockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddLoadedProjection(uint landblockId, WorldEntity entity)
|
private void AddLoadedProjection(
|
||||||
|
uint landblockId,
|
||||||
|
WorldEntity entity,
|
||||||
|
RuntimeEntityKey? requestedKey = null)
|
||||||
{
|
{
|
||||||
LoadedLandblock landblock = _loaded[landblockId];
|
LoadedLandblock landblock = _loaded[landblockId];
|
||||||
List<WorldEntity> entities = MutableEntities(landblock);
|
List<WorldEntity> entities = MutableEntities(landblock);
|
||||||
|
|
@ -763,8 +789,19 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
_animatedIndexByLandblock.Remove(landblockId);
|
_animatedIndexByLandblock.Remove(landblockId);
|
||||||
InvalidateLandblockRenderViews(entries: true, bounds: false);
|
InvalidateLandblockRenderViews(entries: true, bounds: false);
|
||||||
AddFlatEntity(entity);
|
AddFlatEntity(entity);
|
||||||
SetProjectionLocation(entity, landblockId, isLoaded: true, bucketIndex);
|
if (entity.ServerGuid != 0)
|
||||||
AdjustVisibleLiveProjection(entity, +1);
|
{
|
||||||
|
SetProjectionLocation(
|
||||||
|
entity,
|
||||||
|
landblockId,
|
||||||
|
isLoaded: true,
|
||||||
|
bucketIndex,
|
||||||
|
requestedKey);
|
||||||
|
AdjustVisibleLiveProjection(
|
||||||
|
_projectionLocations[entity].Key,
|
||||||
|
entity,
|
||||||
|
+1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveLoadedProjection(WorldEntity entity)
|
private void RemoveLoadedProjection(WorldEntity entity)
|
||||||
|
|
@ -804,7 +841,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
InvalidateLandblockRenderViews(entries: true, bounds: false);
|
InvalidateLandblockRenderViews(entries: true, bounds: false);
|
||||||
RemoveFlatEntity(entity);
|
RemoveFlatEntity(entity);
|
||||||
RemoveProjectionLocation(entity);
|
RemoveProjectionLocation(entity);
|
||||||
AdjustVisibleLiveProjection(entity, -1);
|
AdjustVisibleLiveProjection(location.Key, entity, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveLoadedLandblockFromFlatView(LoadedLandblock landblock)
|
private void RemoveLoadedLandblockFromFlatView(LoadedLandblock landblock)
|
||||||
|
|
@ -813,8 +850,11 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
{
|
{
|
||||||
RemoveFlatEntity(entity);
|
RemoveFlatEntity(entity);
|
||||||
if (entity.ServerGuid != 0)
|
if (entity.ServerGuid != 0)
|
||||||
|
{
|
||||||
|
RuntimeEntityKey key = _projectionLocations[entity].Key;
|
||||||
RemoveProjectionLocation(entity);
|
RemoveProjectionLocation(entity);
|
||||||
AdjustVisibleLiveProjection(entity, -1);
|
AdjustVisibleLiveProjection(key, entity, -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -826,7 +866,13 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
AddFlatEntity(entity);
|
AddFlatEntity(entity);
|
||||||
if (entity.ServerGuid != 0)
|
if (entity.ServerGuid != 0)
|
||||||
SetProjectionLocation(entity, landblock.LandblockId, isLoaded: true, i);
|
SetProjectionLocation(entity, landblock.LandblockId, isLoaded: true, i);
|
||||||
AdjustVisibleLiveProjection(entity, +1);
|
if (entity.ServerGuid != 0)
|
||||||
|
{
|
||||||
|
AdjustVisibleLiveProjection(
|
||||||
|
_projectionLocations[entity].Key,
|
||||||
|
entity,
|
||||||
|
+1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1023,8 +1069,25 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
/// the player walks away.
|
/// the player walks away.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RebucketLiveEntity(WorldEntity entity, uint newCanonicalLb)
|
public void RebucketLiveEntity(WorldEntity entity, uint newCanonicalLb)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
|
RuntimeEntityKey key = _projectionLocations.TryGetValue(
|
||||||
|
entity,
|
||||||
|
out ProjectionLocation existing)
|
||||||
|
? existing.Key
|
||||||
|
: new RuntimeEntityKey(entity.Id, 0);
|
||||||
|
RebucketLiveEntity(key, entity, newCanonicalLb);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RebucketLiveEntity(
|
||||||
|
RuntimeEntityKey key,
|
||||||
|
WorldEntity entity,
|
||||||
|
uint newCanonicalLb)
|
||||||
{
|
{
|
||||||
if (entity.ServerGuid == 0) return;
|
if (entity.ServerGuid == 0) return;
|
||||||
|
if (key.LocalEntityId == 0 || key.LocalEntityId != entity.Id)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The exact rebucket key must match the WorldEntity local ID.");
|
||||||
|
|
||||||
using MutationBatch mutation = BeginMutationBatch();
|
using MutationBatch mutation = BeginMutationBatch();
|
||||||
|
|
||||||
|
|
@ -1032,7 +1095,15 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
|
|
||||||
// Fast path: already drawn in the correct loaded bucket → nothing to do
|
// Fast path: already drawn in the correct loaded bucket → nothing to do
|
||||||
// (avoids per-frame list churn for a settled, stationary entity).
|
// (avoids per-frame list churn for a settled, stationary entity).
|
||||||
if (_projectionLocations.TryGetValue(entity, out ProjectionLocation current)
|
bool hasCurrent = _projectionLocations.TryGetValue(
|
||||||
|
entity,
|
||||||
|
out ProjectionLocation current);
|
||||||
|
if (hasCurrent && current.Key != key)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The live spatial projection is owned by another Runtime incarnation.");
|
||||||
|
}
|
||||||
|
if (hasCurrent
|
||||||
&& current.IsLoaded
|
&& current.IsLoaded
|
||||||
&& current.LandblockId == canonical)
|
&& current.LandblockId == canonical)
|
||||||
{
|
{
|
||||||
|
|
@ -1059,7 +1130,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
// loaded-to-loaded move and lets reentrant visibility callbacks observe
|
// loaded-to-loaded move and lets reentrant visibility callbacks observe
|
||||||
// a state that never exists at the LiveEntityRuntime boundary.
|
// a state that never exists at the LiveEntityRuntime boundary.
|
||||||
RemoveEntityFromAllBuckets(entity);
|
RemoveEntityFromAllBuckets(entity);
|
||||||
PlaceLiveEntityProjection(canonical, entity);
|
PlaceLiveEntityProjection(key, canonical, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1161,10 +1232,17 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
// different landblock by the time the caller reinjects them.
|
// different landblock by the time the caller reinjects them.
|
||||||
var retainedLive = new List<WorldEntity>();
|
var retainedLive = new List<WorldEntity>();
|
||||||
var retainedLiveSet = new HashSet<WorldEntity>(ReferenceEqualityComparer.Instance);
|
var retainedLiveSet = new HashSet<WorldEntity>(ReferenceEqualityComparer.Instance);
|
||||||
|
var retainedLiveKeys = new Dictionary<WorldEntity, RuntimeEntityKey>(
|
||||||
|
ReferenceEqualityComparer.Instance);
|
||||||
void RetainOrRescue(WorldEntity entity, string source)
|
void RetainOrRescue(WorldEntity entity, string source)
|
||||||
{
|
{
|
||||||
if (entity.ServerGuid == 0)
|
if (entity.ServerGuid == 0)
|
||||||
return;
|
return;
|
||||||
|
if (!_projectionLocations.TryGetValue(entity, out ProjectionLocation location))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Live projection 0x{entity.Id:X8} has no exact spatial owner.");
|
||||||
|
}
|
||||||
if (_persistentGuids.Contains(entity.ServerGuid))
|
if (_persistentGuids.Contains(entity.ServerGuid))
|
||||||
{
|
{
|
||||||
_persistentRescued.Add(entity);
|
_persistentRescued.Add(entity);
|
||||||
|
|
@ -1173,7 +1251,10 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (retainedLiveSet.Add(entity))
|
if (retainedLiveSet.Add(entity))
|
||||||
|
{
|
||||||
retainedLive.Add(entity);
|
retainedLive.Add(entity);
|
||||||
|
retainedLiveKeys.Add(entity, location.Key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rescue persistent entities before removal. These get appended
|
// Rescue persistent entities before removal. These get appended
|
||||||
|
|
@ -1219,7 +1300,15 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
RemoveLoadedLandblockFromFlatView(removed);
|
RemoveLoadedLandblockFromFlatView(removed);
|
||||||
|
|
||||||
for (int i = 0; i < retainedLive.Count; i++)
|
for (int i = 0; i < retainedLive.Count; i++)
|
||||||
SetProjectionLocation(retainedLive[i], canonical, isLoaded: false, i);
|
{
|
||||||
|
WorldEntity entity = retainedLive[i];
|
||||||
|
SetProjectionLocation(
|
||||||
|
entity,
|
||||||
|
canonical,
|
||||||
|
isLoaded: false,
|
||||||
|
i,
|
||||||
|
retainedLiveKeys[entity]);
|
||||||
|
}
|
||||||
|
|
||||||
return new GpuLandblockRetirement(
|
return new GpuLandblockRetirement(
|
||||||
canonical,
|
canonical,
|
||||||
|
|
@ -1325,7 +1414,10 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
// Capture live projections from their small dedicated index rather
|
// Capture live projections from their small dedicated index rather
|
||||||
// than walking every DAT-static entity in the 25x25 world window.
|
// than walking every DAT-static entity in the 25x25 world window.
|
||||||
var retainedByLandblock =
|
var retainedByLandblock =
|
||||||
new Dictionary<uint, List<(WorldEntity Entity, int BucketIndex)>>();
|
new Dictionary<uint, List<(
|
||||||
|
WorldEntity Entity,
|
||||||
|
int BucketIndex,
|
||||||
|
RuntimeEntityKey Key)>>();
|
||||||
var rescued = new HashSet<WorldEntity>(
|
var rescued = new HashSet<WorldEntity>(
|
||||||
_persistentRescued,
|
_persistentRescued,
|
||||||
ReferenceEqualityComparer.Instance);
|
ReferenceEqualityComparer.Instance);
|
||||||
|
|
@ -1346,12 +1438,15 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
|
|
||||||
if (!retainedByLandblock.TryGetValue(
|
if (!retainedByLandblock.TryGetValue(
|
||||||
location.LandblockId,
|
location.LandblockId,
|
||||||
out List<(WorldEntity Entity, int BucketIndex)>? retained))
|
out List<(
|
||||||
|
WorldEntity Entity,
|
||||||
|
int BucketIndex,
|
||||||
|
RuntimeEntityKey Key)>? retained))
|
||||||
{
|
{
|
||||||
retained = [];
|
retained = [];
|
||||||
retainedByLandblock.Add(location.LandblockId, retained);
|
retainedByLandblock.Add(location.LandblockId, retained);
|
||||||
}
|
}
|
||||||
retained.Add((entity, location.BucketIndex));
|
retained.Add((entity, location.BucketIndex, location.Key));
|
||||||
}
|
}
|
||||||
|
|
||||||
int spatialOperationCount = OriginRecenterSpatialOperationCount;
|
int spatialOperationCount = OriginRecenterSpatialOperationCount;
|
||||||
|
|
@ -1359,12 +1454,12 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
MutationBatch mutation = BeginMutationBatch();
|
MutationBatch mutation = BeginMutationBatch();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach ((uint localEntityId, int count) in _visibleLiveProjectionCounts)
|
foreach ((RuntimeEntityKey key, int count) in _visibleLiveProjectionCounts)
|
||||||
{
|
{
|
||||||
if (count > 0
|
if (count > 0
|
||||||
&& !_visibilityBeforeMutation.ContainsKey(localEntityId))
|
&& !_visibilityBeforeMutation.ContainsKey(key))
|
||||||
{
|
{
|
||||||
_visibilityBeforeMutation.Add(localEntityId, true);
|
_visibilityBeforeMutation.Add(key, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_visibleLiveProjectionCounts.Clear();
|
_visibleLiveProjectionCounts.Clear();
|
||||||
|
|
@ -1375,7 +1470,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
_flatEntityIndices.Clear();
|
_flatEntityIndices.Clear();
|
||||||
_projectionLocations.Clear();
|
_projectionLocations.Clear();
|
||||||
_loadedLiveByLandblock.Clear();
|
_loadedLiveByLandblock.Clear();
|
||||||
_liveProjectionByLocalId.Clear();
|
_liveProjectionByKey.Clear();
|
||||||
|
|
||||||
_loaded.Clear();
|
_loaded.Clear();
|
||||||
_renderTraversalLandblockSlots.Clear();
|
_renderTraversalLandblockSlots.Clear();
|
||||||
|
|
@ -1393,7 +1488,10 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
_landblockBoundsView.Clear();
|
_landblockBoundsView.Clear();
|
||||||
InvalidateLandblockRenderViews(entries: true, bounds: true);
|
InvalidateLandblockRenderViews(entries: true, bounds: true);
|
||||||
|
|
||||||
foreach ((uint id, List<(WorldEntity Entity, int BucketIndex)> retained) in
|
foreach ((uint id, List<(
|
||||||
|
WorldEntity Entity,
|
||||||
|
int BucketIndex,
|
||||||
|
RuntimeEntityKey Key)> retained) in
|
||||||
retainedByLandblock)
|
retainedByLandblock)
|
||||||
{
|
{
|
||||||
retained.Sort(static (left, right) =>
|
retained.Sort(static (left, right) =>
|
||||||
|
|
@ -1407,7 +1505,8 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
entity,
|
entity,
|
||||||
id,
|
id,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
i);
|
i,
|
||||||
|
retained[i].Key);
|
||||||
}
|
}
|
||||||
_pendingByLandblock.Add(id, pending);
|
_pendingByLandblock.Add(id, pending);
|
||||||
}
|
}
|
||||||
|
|
@ -1552,7 +1651,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
_visibilityBeforeMutation.Clear();
|
_visibilityBeforeMutation.Clear();
|
||||||
_projectionLocations.Clear();
|
_projectionLocations.Clear();
|
||||||
_loadedLiveByLandblock.Clear();
|
_loadedLiveByLandblock.Clear();
|
||||||
_liveProjectionByLocalId.Clear();
|
_liveProjectionByKey.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1578,10 +1677,22 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void PlaceLiveEntityProjection(uint landblockId, WorldEntity entity)
|
public void PlaceLiveEntityProjection(uint landblockId, WorldEntity entity) =>
|
||||||
|
PlaceLiveEntityProjection(
|
||||||
|
new RuntimeEntityKey(entity.Id, 0),
|
||||||
|
landblockId,
|
||||||
|
entity);
|
||||||
|
|
||||||
|
public void PlaceLiveEntityProjection(
|
||||||
|
RuntimeEntityKey key,
|
||||||
|
uint landblockId,
|
||||||
|
WorldEntity entity)
|
||||||
{
|
{
|
||||||
using MutationBatch mutation = BeginMutationBatch();
|
using MutationBatch mutation = BeginMutationBatch();
|
||||||
|
|
||||||
|
if (key.LocalEntityId == 0 || key.LocalEntityId != entity.Id)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The exact placement key must match the WorldEntity local ID.");
|
||||||
if (_projectionLocations.ContainsKey(entity))
|
if (_projectionLocations.ContainsKey(entity))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
|
|
@ -1598,7 +1709,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
{
|
{
|
||||||
// Hot path — append directly to the render-thread-owned mutable
|
// Hot path — append directly to the render-thread-owned mutable
|
||||||
// resident bucket and flat view.
|
// resident bucket and flat view.
|
||||||
AddLoadedProjection(canonicalLandblockId, entity);
|
AddLoadedProjection(canonicalLandblockId, entity, key);
|
||||||
if (probePersistent)
|
if (probePersistent)
|
||||||
EntityVanishProbe.Log($"[ent] APPEND guid=0x{entity.ServerGuid:X8} lb=0x{canonicalLandblockId:X8} -> LOADED(drawn)");
|
EntityVanishProbe.Log($"[ent] APPEND guid=0x{entity.ServerGuid:X8} lb=0x{canonicalLandblockId:X8} -> LOADED(drawn)");
|
||||||
return;
|
return;
|
||||||
|
|
@ -1614,7 +1725,12 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
}
|
}
|
||||||
int bucketIndex = bucket.Count;
|
int bucketIndex = bucket.Count;
|
||||||
bucket.Add(entity);
|
bucket.Add(entity);
|
||||||
SetProjectionLocation(entity, canonicalLandblockId, isLoaded: false, bucketIndex);
|
SetProjectionLocation(
|
||||||
|
entity,
|
||||||
|
canonicalLandblockId,
|
||||||
|
isLoaded: false,
|
||||||
|
bucketIndex,
|
||||||
|
key);
|
||||||
if (probePersistent)
|
if (probePersistent)
|
||||||
EntityVanishProbe.Log($"[ent] APPEND guid=0x{entity.ServerGuid:X8} lb=0x{canonicalLandblockId:X8} -> PENDING(hidden)");
|
EntityVanishProbe.Log($"[ent] APPEND guid=0x{entity.ServerGuid:X8} lb=0x{canonicalLandblockId:X8} -> PENDING(hidden)");
|
||||||
}
|
}
|
||||||
|
|
@ -1837,8 +1953,13 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
resident.Add(entity);
|
resident.Add(entity);
|
||||||
AddFlatEntity(entity);
|
AddFlatEntity(entity);
|
||||||
if (entity.ServerGuid != 0)
|
if (entity.ServerGuid != 0)
|
||||||
|
{
|
||||||
SetProjectionLocation(entity, canonical, isLoaded: true, bucketIndex);
|
SetProjectionLocation(entity, canonical, isLoaded: true, bucketIndex);
|
||||||
AdjustVisibleLiveProjection(entity, +1);
|
AdjustVisibleLiveProjection(
|
||||||
|
_projectionLocations[entity].Key,
|
||||||
|
entity,
|
||||||
|
+1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_animatedIndexByLandblock.Remove(canonical);
|
_animatedIndexByLandblock.Remove(canonical);
|
||||||
InvalidateLandblockRenderViews(entries: true, bounds: false);
|
InvalidateLandblockRenderViews(entries: true, bounds: false);
|
||||||
|
|
@ -1873,8 +1994,8 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
((Action<uint, bool>)subscribers[i])(
|
((Action<RuntimeEntityKey, bool>)subscribers[i])(
|
||||||
transition.LocalEntityId,
|
transition.Key,
|
||||||
transition.Visible);
|
transition.Visible);
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch (Exception error)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AcDream.App.Audio;
|
using AcDream.App.Audio;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.App.Streaming;
|
namespace AcDream.App.Streaming;
|
||||||
|
|
||||||
|
|
@ -62,22 +63,22 @@ internal sealed class WorldGenerationQuiescence
|
||||||
private readonly WorldGenerationAvailabilityState _availability;
|
private readonly WorldGenerationAvailabilityState _availability;
|
||||||
private readonly SelectionState _selection;
|
private readonly SelectionState _selection;
|
||||||
private readonly GpuWorldState _world;
|
private readonly GpuWorldState _world;
|
||||||
private readonly Func<uint, uint?> _resolveLocalEntityId;
|
private readonly Func<uint, RuntimeEntityKey?> _resolveProjectionKey;
|
||||||
private readonly IWorldAudioQuiescence? _audio;
|
private readonly IWorldAudioQuiescence? _audio;
|
||||||
|
|
||||||
public WorldGenerationQuiescence(
|
public WorldGenerationQuiescence(
|
||||||
WorldGenerationAvailabilityState availability,
|
WorldGenerationAvailabilityState availability,
|
||||||
SelectionState selection,
|
SelectionState selection,
|
||||||
GpuWorldState world,
|
GpuWorldState world,
|
||||||
Func<uint, uint?> resolveLocalEntityId,
|
Func<uint, RuntimeEntityKey?> resolveProjectionKey,
|
||||||
IWorldAudioQuiescence? audio)
|
IWorldAudioQuiescence? audio)
|
||||||
{
|
{
|
||||||
_availability = availability
|
_availability = availability
|
||||||
?? throw new ArgumentNullException(nameof(availability));
|
?? throw new ArgumentNullException(nameof(availability));
|
||||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||||
_resolveLocalEntityId = resolveLocalEntityId
|
_resolveProjectionKey = resolveProjectionKey
|
||||||
?? throw new ArgumentNullException(nameof(resolveLocalEntityId));
|
?? throw new ArgumentNullException(nameof(resolveProjectionKey));
|
||||||
_audio = audio;
|
_audio = audio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,8 +90,8 @@ internal sealed class WorldGenerationQuiescence
|
||||||
uint? selected = firstEdge ? _selection.SelectedObjectId : null;
|
uint? selected = firstEdge ? _selection.SelectedObjectId : null;
|
||||||
bool clearWorldSelection =
|
bool clearWorldSelection =
|
||||||
selected is uint selectedGuid
|
selected is uint selectedGuid
|
||||||
&& _resolveLocalEntityId(selectedGuid) is uint localEntityId
|
&& _resolveProjectionKey(selectedGuid) is RuntimeEntityKey key
|
||||||
&& _world.IsLiveEntityVisible(localEntityId);
|
&& _world.IsLiveEntityVisible(key);
|
||||||
|
|
||||||
_availability.Begin(generation);
|
_availability.Begin(generation);
|
||||||
if (!firstEdge)
|
if (!firstEdge)
|
||||||
|
|
|
||||||
|
|
@ -951,7 +951,10 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_spatial.RebucketLiveEntity(entity, spatialCellOrLandblockId);
|
_spatial.RebucketLiveEntity(
|
||||||
|
key,
|
||||||
|
entity,
|
||||||
|
spatialCellOrLandblockId);
|
||||||
}
|
}
|
||||||
catch (AggregateException error)
|
catch (AggregateException error)
|
||||||
{
|
{
|
||||||
|
|
@ -974,7 +977,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
runtimeNotificationFailure: null);
|
runtimeNotificationFailure: null);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bool visible = _spatial.IsLiveEntityProjectionResident(entity.Id);
|
bool visible = _spatial.IsLiveEntityProjectionResident(key);
|
||||||
record.IsSpatiallyVisible = visible;
|
record.IsSpatiallyVisible = visible;
|
||||||
RefreshPresentation(record);
|
RefreshPresentation(record);
|
||||||
if ((spatialCellOrLandblockId & 0xFFFFu) != 0xFFFFu)
|
if ((spatialCellOrLandblockId & 0xFFFFu) != 0xFFFFu)
|
||||||
|
|
@ -1483,6 +1486,23 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal bool TryGetProjectionKey(
|
||||||
|
uint serverGuid,
|
||||||
|
out RuntimeEntityKey key)
|
||||||
|
{
|
||||||
|
if (_projections.TryGetCurrent(
|
||||||
|
serverGuid,
|
||||||
|
out LiveEntityRecord? record)
|
||||||
|
&& record.ProjectionKey is { } found)
|
||||||
|
{
|
||||||
|
key = found;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
key = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public bool ClearAnimationRuntime(uint serverGuid)
|
public bool ClearAnimationRuntime(uint serverGuid)
|
||||||
{
|
{
|
||||||
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
||||||
|
|
@ -2942,25 +2962,26 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSpatialVisibilityChanged(uint localEntityId, bool visible)
|
private void OnSpatialVisibilityChanged(RuntimeEntityKey key, bool visible)
|
||||||
{
|
{
|
||||||
// GpuWorldState serializes re-entrant visibility callbacks. A callback
|
// GpuWorldState serializes re-entrant visibility callbacks. A callback
|
||||||
// may delete an incarnation and materialize the same server GUID before
|
// may delete an incarnation and materialize the same server GUID before
|
||||||
// an older queued edge drains. Apply an edge only while it still
|
// an older queued edge drains. Apply an edge only while it still
|
||||||
// matches current spatial truth; otherwise it belongs to the displaced
|
// matches current spatial truth; otherwise it belongs to the displaced
|
||||||
// projection and must not mutate the replacement generation.
|
// projection and must not mutate the replacement generation.
|
||||||
if (_spatial.IsLiveEntityProjectionResident(localEntityId) != visible)
|
if (_spatial.IsLiveEntityProjectionResident(key) != visible)
|
||||||
return;
|
return;
|
||||||
if (!_projections.TryGetByLocalId(
|
if (!_projections.TryGet(
|
||||||
localEntityId,
|
key,
|
||||||
out LiveEntityRecord? record)
|
out LiveEntityRecord? record)
|
||||||
|| record.WorldEntity is not { } entity
|
|| record.WorldEntity is not { } entity
|
||||||
|| entity.Id != localEntityId)
|
|| entity.Id != key.LocalEntityId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
uint serverGuid = record.ServerGuid;
|
uint serverGuid = record.ServerGuid;
|
||||||
bool wasVisible = record.IsSpatiallyVisible;
|
bool wasVisible = record.IsSpatiallyVisible;
|
||||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
if (RequireProjectionKey(record) != key)
|
||||||
|
return;
|
||||||
bool wasOrdinaryRoot = _spatialRootObjects.TryGetValue(
|
bool wasOrdinaryRoot = _spatialRootObjects.TryGetValue(
|
||||||
key,
|
key,
|
||||||
out LiveEntityRecord? indexedRoot)
|
out LiveEntityRecord? indexedRoot)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Streaming;
|
namespace AcDream.App.Tests.Streaming;
|
||||||
|
|
@ -227,15 +228,16 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Array.Empty<WorldEntity>()));
|
Array.Empty<WorldEntity>()));
|
||||||
WorldEntity entity = Entity(1u, 0x73600001u);
|
WorldEntity entity = Entity(1u, 0x73600001u);
|
||||||
state.PlaceLiveEntityProjection(landblock, entity);
|
state.PlaceLiveEntityProjection(landblock, entity);
|
||||||
var edges = new List<(uint LocalEntityId, bool Visible)>();
|
RuntimeEntityKey key = Key(entity);
|
||||||
state.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
var edges = new List<(RuntimeEntityKey Key, bool Visible)>();
|
||||||
edges.Add((localEntityId, visible));
|
state.LiveProjectionVisibilityChanged += (observedKey, visible) =>
|
||||||
|
edges.Add((observedKey, visible));
|
||||||
|
|
||||||
state.RemoveLandblock(landblock);
|
state.RemoveLandblock(landblock);
|
||||||
|
|
||||||
Assert.Empty(state.Entities);
|
Assert.Empty(state.Entities);
|
||||||
Assert.Equal(1, state.PendingLiveEntityCount);
|
Assert.Equal(1, state.PendingLiveEntityCount);
|
||||||
Assert.False(state.IsLiveEntityVisible(entity.Id));
|
Assert.False(state.IsLiveEntityVisible(key));
|
||||||
|
|
||||||
state.AddLandblock(new LoadedLandblock(
|
state.AddLandblock(new LoadedLandblock(
|
||||||
landblock,
|
landblock,
|
||||||
|
|
@ -245,9 +247,9 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Assert.Same(entity, Assert.Single(state.Entities));
|
Assert.Same(entity, Assert.Single(state.Entities));
|
||||||
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? reloaded));
|
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? reloaded));
|
||||||
Assert.Same(entity, Assert.Single(reloaded!.Entities));
|
Assert.Same(entity, Assert.Single(reloaded!.Entities));
|
||||||
Assert.True(state.IsLiveEntityVisible(entity.Id));
|
Assert.True(state.IsLiveEntityVisible(key));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[(entity.Id, false), (entity.Id, true)],
|
[(key, false), (key, true)],
|
||||||
edges);
|
edges);
|
||||||
|
|
||||||
state.RemoveLiveEntityProjection(entity);
|
state.RemoveLiveEntityProjection(entity);
|
||||||
|
|
@ -280,9 +282,9 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
state.MarkPersistent(playerGuid);
|
state.MarkPersistent(playerGuid);
|
||||||
state.PlaceLiveEntityProjection(secondLandblock, player);
|
state.PlaceLiveEntityProjection(secondLandblock, player);
|
||||||
state.PlaceLiveEntityProjection(pendingLandblock, pending);
|
state.PlaceLiveEntityProjection(pendingLandblock, pending);
|
||||||
var edges = new List<(uint LocalEntityId, bool Visible)>();
|
var edges = new List<(RuntimeEntityKey Key, bool Visible)>();
|
||||||
state.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
state.LiveProjectionVisibilityChanged += (key, visible) =>
|
||||||
edges.Add((localEntityId, visible));
|
edges.Add((key, visible));
|
||||||
|
|
||||||
GpuWorldRecenterRetirement result =
|
GpuWorldRecenterRetirement result =
|
||||||
state.DetachAllForOriginRecenter();
|
state.DetachAllForOriginRecenter();
|
||||||
|
|
@ -317,7 +319,7 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Assert.Equal(2, state.PendingLiveEntityCount);
|
Assert.Equal(2, state.PendingLiveEntityCount);
|
||||||
Assert.Same(player, Assert.Single(state.DrainRescued()));
|
Assert.Same(player, Assert.Single(state.DrainRescued()));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[(remote.Id, false), (player.Id, false)],
|
[(Key(remote), false), (Key(player), false)],
|
||||||
edges);
|
edges);
|
||||||
|
|
||||||
state.AddLandblock(new LoadedLandblock(
|
state.AddLandblock(new LoadedLandblock(
|
||||||
|
|
@ -379,12 +381,12 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Array.Empty<WorldEntity>()));
|
Array.Empty<WorldEntity>()));
|
||||||
WorldEntity entity = Entity(1u, 0x73700001u);
|
WorldEntity entity = Entity(1u, 0x73700001u);
|
||||||
int callbacks = 0;
|
int callbacks = 0;
|
||||||
state.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
state.LiveProjectionVisibilityChanged += (key, visible) =>
|
||||||
{
|
{
|
||||||
callbacks++;
|
callbacks++;
|
||||||
Assert.Equal(entity.Id, localEntityId);
|
Assert.Equal(entity.Id, key.LocalEntityId);
|
||||||
Assert.True(visible);
|
Assert.True(visible);
|
||||||
Assert.True(state.IsLiveEntityVisible(localEntityId));
|
Assert.True(state.IsLiveEntityVisible(key));
|
||||||
Assert.Same(entity, Assert.Single(state.Entities));
|
Assert.Same(entity, Assert.Single(state.Entities));
|
||||||
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? loaded));
|
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? loaded));
|
||||||
Assert.Same(entity, Assert.Single(loaded!.Entities));
|
Assert.Same(entity, Assert.Single(loaded!.Entities));
|
||||||
|
|
@ -415,14 +417,14 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Array.Empty<WorldEntity>()));
|
Array.Empty<WorldEntity>()));
|
||||||
WorldEntity entity = Entity(1u, 0x73800001u);
|
WorldEntity entity = Entity(1u, 0x73800001u);
|
||||||
state.PlaceLiveEntityProjection(sourceLandblock, entity);
|
state.PlaceLiveEntityProjection(sourceLandblock, entity);
|
||||||
var edges = new List<(uint LocalEntityId, bool Visible)>();
|
var edges = new List<(RuntimeEntityKey Key, bool Visible)>();
|
||||||
state.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
state.LiveProjectionVisibilityChanged += (key, visible) =>
|
||||||
edges.Add((localEntityId, visible));
|
edges.Add((key, visible));
|
||||||
|
|
||||||
state.RebucketLiveEntity(entity, targetLandblock);
|
state.RebucketLiveEntity(entity, targetLandblock);
|
||||||
|
|
||||||
Assert.Empty(edges);
|
Assert.Empty(edges);
|
||||||
Assert.True(state.IsLiveEntityVisible(entity.Id));
|
Assert.True(state.IsLiveEntityVisible(Key(entity)));
|
||||||
Assert.True(state.TryGetLandblock(sourceLandblock, out LoadedLandblock? source));
|
Assert.True(state.TryGetLandblock(sourceLandblock, out LoadedLandblock? source));
|
||||||
Assert.Empty(source!.Entities);
|
Assert.Empty(source!.Entities);
|
||||||
Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target));
|
Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target));
|
||||||
|
|
@ -430,6 +432,64 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Assert.Same(entity, Assert.Single(state.Entities));
|
Assert.Same(entity, Assert.Single(state.Entities));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadedDetachAndReload_PreserveExactIncarnationKey()
|
||||||
|
{
|
||||||
|
const uint landblock = 0x0101FFFFu;
|
||||||
|
var state = new GpuWorldState();
|
||||||
|
state.AddLandblock(new LoadedLandblock(
|
||||||
|
landblock,
|
||||||
|
new LandBlock(),
|
||||||
|
Array.Empty<WorldEntity>()));
|
||||||
|
WorldEntity entity = Entity(1u, 0x73800011u);
|
||||||
|
var exactKey = new RuntimeEntityKey(entity.Id, 37);
|
||||||
|
state.PlaceLiveEntityProjection(exactKey, landblock, entity);
|
||||||
|
var edges = new List<(RuntimeEntityKey Key, bool Visible)>();
|
||||||
|
state.LiveProjectionVisibilityChanged += (key, visible) =>
|
||||||
|
edges.Add((key, visible));
|
||||||
|
|
||||||
|
state.DetachLandblock(landblock);
|
||||||
|
|
||||||
|
Assert.Equal([(exactKey, false)], edges);
|
||||||
|
Assert.False(state.IsLiveEntityVisible(exactKey));
|
||||||
|
Assert.Empty(state.Entities);
|
||||||
|
|
||||||
|
state.AddLandblock(new LoadedLandblock(
|
||||||
|
landblock,
|
||||||
|
new LandBlock(),
|
||||||
|
Array.Empty<WorldEntity>()));
|
||||||
|
|
||||||
|
Assert.Equal([(exactKey, false), (exactKey, true)], edges);
|
||||||
|
Assert.True(state.IsLiveEntityVisible(exactKey));
|
||||||
|
Assert.Same(entity, Assert.Single(state.Entities));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PendingRebucket_WithWrongIncarnation_IsRejectedWithoutMutation()
|
||||||
|
{
|
||||||
|
const uint sourceLandblock = 0x0101FFFFu;
|
||||||
|
const uint targetLandblock = 0x0202FFFFu;
|
||||||
|
var state = new GpuWorldState();
|
||||||
|
WorldEntity entity = Entity(1u, 0x73800012u);
|
||||||
|
var exactKey = new RuntimeEntityKey(entity.Id, 37);
|
||||||
|
state.PlaceLiveEntityProjection(exactKey, sourceLandblock, entity);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
state.RebucketLiveEntity(
|
||||||
|
new RuntimeEntityKey(entity.Id, 38),
|
||||||
|
entity,
|
||||||
|
targetLandblock));
|
||||||
|
|
||||||
|
Assert.Equal(1, state.PendingLiveEntityCount);
|
||||||
|
Assert.False(state.IsLiveEntityProjectionResident(exactKey));
|
||||||
|
state.AddLandblock(new LoadedLandblock(
|
||||||
|
sourceLandblock,
|
||||||
|
new LandBlock(),
|
||||||
|
Array.Empty<WorldEntity>()));
|
||||||
|
Assert.True(state.IsLiveEntityVisible(exactKey));
|
||||||
|
Assert.Same(entity, Assert.Single(state.Entities));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SameGuidOverlap_TracksExactProjectionVisibilityIndependently()
|
public void SameGuidOverlap_TracksExactProjectionVisibilityIndependently()
|
||||||
{
|
{
|
||||||
|
|
@ -444,21 +504,21 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
WorldEntity second = Entity(2u, guid);
|
WorldEntity second = Entity(2u, guid);
|
||||||
state.PlaceLiveEntityProjection(landblock, first);
|
state.PlaceLiveEntityProjection(landblock, first);
|
||||||
state.PlaceLiveEntityProjection(landblock, second);
|
state.PlaceLiveEntityProjection(landblock, second);
|
||||||
var edges = new List<(uint LocalEntityId, bool Visible)>();
|
var edges = new List<(RuntimeEntityKey Key, bool Visible)>();
|
||||||
state.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
state.LiveProjectionVisibilityChanged += (key, visible) =>
|
||||||
edges.Add((localEntityId, visible));
|
edges.Add((key, visible));
|
||||||
|
|
||||||
state.RemoveLiveEntityProjection(first);
|
state.RemoveLiveEntityProjection(first);
|
||||||
|
|
||||||
Assert.Equal([(first.Id, false)], edges);
|
Assert.Equal([(Key(first), false)], edges);
|
||||||
Assert.False(state.IsLiveEntityVisible(first.Id));
|
Assert.False(state.IsLiveEntityVisible(Key(first)));
|
||||||
Assert.True(state.IsLiveEntityVisible(second.Id));
|
Assert.True(state.IsLiveEntityVisible(Key(second)));
|
||||||
Assert.Same(second, Assert.Single(state.Entities));
|
Assert.Same(second, Assert.Single(state.Entities));
|
||||||
|
|
||||||
state.RemoveLiveEntityProjection(guid);
|
state.RemoveLiveEntityProjection(guid);
|
||||||
|
|
||||||
Assert.Equal([(first.Id, false), (second.Id, false)], edges);
|
Assert.Equal([(Key(first), false), (Key(second), false)], edges);
|
||||||
Assert.False(state.IsLiveEntityVisible(second.Id));
|
Assert.False(state.IsLiveEntityVisible(Key(second)));
|
||||||
Assert.Empty(state.Entities);
|
Assert.Empty(state.Entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -491,11 +551,11 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
var state = new GpuWorldState();
|
var state = new GpuWorldState();
|
||||||
state.PlaceLiveEntityProjection(landblock, first);
|
state.PlaceLiveEntityProjection(landblock, first);
|
||||||
state.PlaceLiveEntityProjection(landblock, second);
|
state.PlaceLiveEntityProjection(landblock, second);
|
||||||
var observed = new List<(uint LocalEntityId, bool Visible)>();
|
var observed = new List<(RuntimeEntityKey Key, bool Visible)>();
|
||||||
state.LiveProjectionVisibilityChanged += (_, _) =>
|
state.LiveProjectionVisibilityChanged += (_, _) =>
|
||||||
throw new InvalidOperationException("fixture observer failure");
|
throw new InvalidOperationException("fixture observer failure");
|
||||||
state.LiveProjectionVisibilityChanged += (guid, visible) =>
|
state.LiveProjectionVisibilityChanged += (key, visible) =>
|
||||||
observed.Add((guid, visible));
|
observed.Add((key, visible));
|
||||||
|
|
||||||
AggregateException error = Assert.Throws<AggregateException>(() =>
|
AggregateException error = Assert.Throws<AggregateException>(() =>
|
||||||
state.AddLandblock(new LoadedLandblock(
|
state.AddLandblock(new LoadedLandblock(
|
||||||
|
|
@ -505,10 +565,10 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
|
|
||||||
Assert.Equal(2, error.InnerExceptions.Count);
|
Assert.Equal(2, error.InnerExceptions.Count);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[(first.Id, true), (second.Id, true)],
|
[(Key(first), true), (Key(second), true)],
|
||||||
observed.OrderBy(edge => edge.LocalEntityId).ToArray());
|
observed.OrderBy(edge => edge.Key.LocalEntityId).ToArray());
|
||||||
Assert.True(state.IsLiveEntityVisible(first.Id));
|
Assert.True(state.IsLiveEntityVisible(Key(first)));
|
||||||
Assert.True(state.IsLiveEntityVisible(second.Id));
|
Assert.True(state.IsLiveEntityVisible(Key(second)));
|
||||||
Assert.Equal(0, state.PendingVisibilityTransitionCount);
|
Assert.Equal(0, state.PendingVisibilityTransitionCount);
|
||||||
Assert.Equal(2, state.Entities.Count);
|
Assert.Equal(2, state.Entities.Count);
|
||||||
}
|
}
|
||||||
|
|
@ -578,13 +638,14 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
id => Entity(id, guid));
|
id => Entity(id, guid));
|
||||||
|
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||||
Assert.False(state.IsLiveEntityVisible(record.WorldEntity!.Id));
|
RuntimeEntityKey projectionKey = record.ProjectionKey!.Value;
|
||||||
Assert.True(state.IsLiveEntityProjectionResident(record.WorldEntity.Id));
|
Assert.False(state.IsLiveEntityVisible(projectionKey));
|
||||||
|
Assert.True(state.IsLiveEntityProjectionResident(projectionKey));
|
||||||
Assert.True(record.IsSpatiallyVisible);
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
Assert.Equal([true], edges);
|
Assert.Equal([true], edges);
|
||||||
|
|
||||||
Assert.True(availability.End(7));
|
Assert.True(availability.End(7));
|
||||||
Assert.True(state.IsLiveEntityVisible(record.WorldEntity.Id));
|
Assert.True(state.IsLiveEntityVisible(projectionKey));
|
||||||
Assert.True(record.IsSpatiallyVisible);
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -670,6 +731,9 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static RuntimeEntityKey Key(WorldEntity entity) =>
|
||||||
|
new(entity.Id, 0);
|
||||||
|
|
||||||
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell)
|
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell)
|
||||||
{
|
{
|
||||||
var position = new CreateObject.ServerPosition(
|
var position = new CreateObject.ServerPosition(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using AcDream.App.Audio;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Streaming;
|
namespace AcDream.App.Tests.Streaming;
|
||||||
|
|
@ -23,10 +24,11 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
new LandBlock(),
|
new LandBlock(),
|
||||||
Array.Empty<WorldEntity>()));
|
Array.Empty<WorldEntity>()));
|
||||||
world.PlaceLiveEntityProjection(LandblockId, entity);
|
world.PlaceLiveEntityProjection(LandblockId, entity);
|
||||||
|
RuntimeEntityKey key = Key(entity);
|
||||||
world.SetLandblockAabb(LandblockId, Vector3.Zero, Vector3.One);
|
world.SetLandblockAabb(LandblockId, Vector3.Zero, Vector3.One);
|
||||||
var nearby = new List<KeyValuePair<uint, WorldEntity>>();
|
var nearby = new List<KeyValuePair<uint, WorldEntity>>();
|
||||||
|
|
||||||
Assert.True(world.IsLiveEntityVisible(entity.Id));
|
Assert.True(world.IsLiveEntityVisible(key));
|
||||||
Assert.Single(world.LandblockEntries);
|
Assert.Single(world.LandblockEntries);
|
||||||
Assert.Single(world.LandblockBounds);
|
Assert.Single(world.LandblockBounds);
|
||||||
|
|
||||||
|
|
@ -34,7 +36,7 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby);
|
world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby);
|
||||||
|
|
||||||
Assert.False(availability.IsWorldAvailable);
|
Assert.False(availability.IsWorldAvailable);
|
||||||
Assert.False(world.IsLiveEntityVisible(entity.Id));
|
Assert.False(world.IsLiveEntityVisible(key));
|
||||||
Assert.Empty(world.LandblockEntries);
|
Assert.Empty(world.LandblockEntries);
|
||||||
Assert.Empty(world.LandblockBounds);
|
Assert.Empty(world.LandblockBounds);
|
||||||
Assert.Empty(nearby);
|
Assert.Empty(nearby);
|
||||||
|
|
@ -44,7 +46,7 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
Assert.False(availability.End(16));
|
Assert.False(availability.End(16));
|
||||||
Assert.False(availability.IsWorldAvailable);
|
Assert.False(availability.IsWorldAvailable);
|
||||||
Assert.True(availability.End(17));
|
Assert.True(availability.End(17));
|
||||||
Assert.True(world.IsLiveEntityVisible(entity.Id));
|
Assert.True(world.IsLiveEntityVisible(key));
|
||||||
Assert.Same(entity, Assert.Single(world.LandblockEntries).Entities.Single());
|
Assert.Same(entity, Assert.Single(world.LandblockEntries).Entities.Single());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +68,7 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
availability,
|
availability,
|
||||||
selection,
|
selection,
|
||||||
world,
|
world,
|
||||||
guid => guid == ServerGuid ? entity.Id : null,
|
guid => guid == ServerGuid ? Key(entity) : null,
|
||||||
audio);
|
audio);
|
||||||
|
|
||||||
quiescence.Begin(1);
|
quiescence.Begin(1);
|
||||||
|
|
@ -115,6 +117,9 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static RuntimeEntityKey Key(WorldEntity entity) =>
|
||||||
|
new(entity.Id, 0);
|
||||||
|
|
||||||
private sealed class RecordingAudioQuiescence : IWorldAudioQuiescence
|
private sealed class RecordingAudioQuiescence : IWorldAudioQuiescence
|
||||||
{
|
{
|
||||||
public int SuspendCalls { get; private set; }
|
public int SuspendCalls { get; private set; }
|
||||||
|
|
|
||||||
|
|
@ -2168,9 +2168,9 @@ public sealed class LiveEntityRuntimeTests
|
||||||
0x01010001u,
|
0x01010001u,
|
||||||
id => Entity(id, guid))!;
|
id => Entity(id, guid))!;
|
||||||
bool replaced = false;
|
bool replaced = false;
|
||||||
spatial.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
spatial.LiveProjectionVisibilityChanged += (key, visible) =>
|
||||||
{
|
{
|
||||||
if (replaced || visible || localEntityId != original.Id)
|
if (replaced || visible || key.LocalEntityId != original.Id)
|
||||||
return;
|
return;
|
||||||
replaced = true;
|
replaced = true;
|
||||||
Assert.True(runtime.UnregisterLiveEntity(
|
Assert.True(runtime.UnregisterLiveEntity(
|
||||||
|
|
@ -2325,9 +2325,9 @@ public sealed class LiveEntityRuntimeTests
|
||||||
0x01010001u,
|
0x01010001u,
|
||||||
id => Entity(id, guid))!;
|
id => Entity(id, guid))!;
|
||||||
bool rebucketed = false;
|
bool rebucketed = false;
|
||||||
spatial.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
spatial.LiveProjectionVisibilityChanged += (key, visible) =>
|
||||||
{
|
{
|
||||||
if (rebucketed || visible || localEntityId != original.Id)
|
if (rebucketed || visible || key.LocalEntityId != original.Id)
|
||||||
return;
|
return;
|
||||||
rebucketed = true;
|
rebucketed = true;
|
||||||
Assert.True(runtime.RebucketLiveEntity(guid, 0x03030033u));
|
Assert.True(runtime.RebucketLiveEntity(guid, 0x03030033u));
|
||||||
|
|
@ -2383,7 +2383,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal((ushort)2, replacement.Generation);
|
Assert.Equal((ushort)2, replacement.Generation);
|
||||||
Assert.True(replacement.IsSpatiallyVisible);
|
Assert.True(replacement.IsSpatiallyVisible);
|
||||||
Assert.True(spatial.IsLiveEntityVisible(
|
Assert.True(spatial.IsLiveEntityVisible(
|
||||||
replacement.WorldEntity!.Id));
|
replacement.ProjectionKey!.Value));
|
||||||
Assert.Equal(2, resources.RegisterCount);
|
Assert.Equal(2, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
|
|
@ -84,6 +85,11 @@ public sealed class RuntimeEntityOwnershipTests
|
||||||
AssertExactKeyFields(
|
AssertExactKeyFields(
|
||||||
typeof(RemoteMovementObservationTracker),
|
typeof(RemoteMovementObservationTracker),
|
||||||
"_lastMove");
|
"_lastMove");
|
||||||
|
AssertExactKeyFields(
|
||||||
|
typeof(GpuWorldState),
|
||||||
|
"_liveProjectionByKey",
|
||||||
|
"_visibleLiveProjectionCounts",
|
||||||
|
"_visibilityBeforeMutation");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue