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