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