refactor(app): key live projections by runtime identity

Move materialized live-object sidecars and presentation worksets to exact RuntimeEntityKey ownership. Runtime remains the only GUID/incarnation/local-ID authority while hydration, animation, effects, lights, equipped children, renderer resources, visibility, liveness, and teardown resolve exact projection identities. Preserve synchronous callbacks, local-ID allocation order, and current rendering behavior.
This commit is contained in:
Erik 2026-07-25 21:50:58 +02:00
parent e18df84437
commit 420e5eea70
73 changed files with 2939 additions and 1715 deletions

View file

@ -78,14 +78,16 @@ public sealed class LiveEntityHydrationControllerTests
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
Assert.Null(record.WorldEntity);
RuntimeEntityRecord canonical = fixture.Canonical;
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
Assert.Null(canonical.LocalEntityId);
Assert.Equal(0, fixture.Resources.RegisterCount);
Assert.Empty(fixture.Materializer.Calls);
fixture.Origin.IsKnownValue = true;
fixture.Controller.OnLandblockLoaded(Cell);
LiveEntityRecord record = fixture.Record;
Assert.NotNull(record.WorldEntity);
Assert.Single(fixture.Materializer.Calls);
Assert.Equal(LiveProjectionPurpose.SpatialRecovery, fixture.Materializer.Calls[0].Purpose);
@ -130,11 +132,13 @@ public sealed class LiveEntityHydrationControllerTests
fixture.Materializer.SkipMaterializationCount = 1;
WorldSession.EntitySpawn spawn = Spawn(Generation: 1, PositionSequence: 1);
fixture.Controller.OnCreate(spawn);
LiveEntityRecord record = fixture.Record;
Assert.Null(record.WorldEntity);
RuntimeEntityRecord canonical = fixture.Canonical;
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
Assert.Null(canonical.LocalEntityId);
Assert.True(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u)));
LiveEntityRecord record = fixture.Record;
Assert.NotNull(record.WorldEntity);
Assert.Equal(2, fixture.Materializer.Calls.Count);
Assert.Equal(LiveProjectionPurpose.SpatialRecovery, fixture.Materializer.Calls[1].Purpose);
@ -516,13 +520,15 @@ public sealed class LiveEntityHydrationControllerTests
Assert.Throws<InvalidOperationException>(() =>
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)));
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
Assert.Null(record.WorldEntity);
RuntimeEntityRecord canonical = fixture.Canonical;
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
Assert.Null(canonical.LocalEntityId);
Assert.Equal(1, resources.RegisterCount);
Assert.Equal(1, resources.UnregisterCount);
fixture.Controller.OnLandblockLoaded(Cell);
LiveEntityRecord record = fixture.Record;
Assert.NotNull(record.WorldEntity);
Assert.Equal(2, resources.RegisterCount);
Assert.Equal(1, resources.UnregisterCount);
@ -644,7 +650,9 @@ public sealed class LiveEntityHydrationControllerTests
ClientObject retainedObject = fixture.Objects.Get(Guid)!;
Assert.True(fixture.Controller.OnPrune(
new LiveEntityPruneCandidate(Guid, Generation: 1)));
new LiveEntityPruneCandidate(
fixture.Record.ProjectionKey!.Value,
Guid)));
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
Assert.Same(retainedObject, fixture.Objects.Get(Guid));
@ -658,7 +666,9 @@ public sealed class LiveEntityHydrationControllerTests
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
Assert.True(fixture.Controller.OnPrune(
new LiveEntityPruneCandidate(Guid, Generation: 1)));
new LiveEntityPruneCandidate(
fixture.Record.ProjectionKey!.Value,
Guid)));
fixture.Controller.OnLandblockLoaded(Cell);
@ -677,7 +687,9 @@ public sealed class LiveEntityHydrationControllerTests
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
Assert.True(fixture.Controller.OnPrune(
new LiveEntityPruneCandidate(Guid, Generation: 1)));
new LiveEntityPruneCandidate(
fixture.Record.ProjectionKey!.Value,
Guid)));
Assert.True(fixture.Controller.OnDelete(
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
@ -694,7 +706,9 @@ public sealed class LiveEntityHydrationControllerTests
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 1));
Assert.True(fixture.Controller.OnPrune(
new LiveEntityPruneCandidate(Guid, Generation: 2)));
new LiveEntityPruneCandidate(
fixture.Record.ProjectionKey!.Value,
Guid)));
fixture.Controller.OnCreate(Spawn(
Generation: 1,
@ -723,7 +737,9 @@ public sealed class LiveEntityHydrationControllerTests
PositionSequence: 5);
fixture.Controller.OnCreate(accepted);
Assert.True(fixture.Controller.OnPrune(
new LiveEntityPruneCandidate(Guid, Generation: 1)));
new LiveEntityPruneCandidate(
fixture.Record.ProjectionKey!.Value,
Guid)));
CreateObject.ServerPosition stalePosition =
accepted.Position!.Value with
@ -760,7 +776,9 @@ public sealed class LiveEntityHydrationControllerTests
Value = 123,
});
Assert.True(fixture.Controller.OnPrune(
new LiveEntityPruneCandidate(Guid, Generation: 1)));
new LiveEntityPruneCandidate(
fixture.Record.ProjectionKey!.Value,
Guid)));
fixture.Controller.OnCreate(
Spawn(Generation: 2, PositionSequence: 1) with
@ -831,10 +849,11 @@ public sealed class LiveEntityHydrationControllerTests
Assert.True(fixture.Controller.OnDelete(
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord replacement));
Assert.NotSame(old, replacement);
RuntimeEntityRecord replacement = fixture.Canonical;
Assert.Equal((ushort)2, replacement.Generation);
Assert.Equal("replacement", replacement.Snapshot.Name);
Assert.Null(replacement.LocalEntityId);
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
Assert.Equal(0, fixture.Runtime.PendingTeardownCount);
}
@ -1348,10 +1367,9 @@ public sealed class LiveEntityHydrationControllerTests
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
Assert.False(record.CreateProjectionSynchronizationPending);
Assert.False(record.InitialHydrationCompleted);
Assert.Null(record.WorldEntity);
RuntimeEntityRecord canonical = fixture.Canonical;
Assert.Null(canonical.LocalEntityId);
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
Assert.Equal(0, fixture.Resources.RegisterCount);
fixture.Materializer.BeforeMaterialize = null;
@ -1371,6 +1389,7 @@ public sealed class LiveEntityHydrationControllerTests
};
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 3));
LiveEntityRecord record = fixture.Record;
Assert.True(record.InitialHydrationCompleted);
Assert.NotNull(record.WorldEntity);
Assert.Equal(1, fixture.Resources.RegisterCount);
@ -1481,12 +1500,14 @@ public sealed class LiveEntityHydrationControllerTests
};
fixture.Relationships.OnSpawnAction = _ =>
{
LiveEntityRecord record = fixture.Record;
if (record.WorldEntity is not null)
RuntimeEntityRecord canonical = fixture.Canonical;
if (fixture.Runtime.TryGetRecord(
Guid,
out LiveEntityRecord _))
return;
WorldEntity? attached = fixture.Runtime.MaterializeLiveEntity(
Guid,
canonical,
Cell,
id => new WorldEntity
{
@ -1498,10 +1519,13 @@ public sealed class LiveEntityHydrationControllerTests
MeshRefs = [],
ParentCellId = Cell,
},
LiveEntityProjectionKind.Attached);
LiveEntityProjectionKind.Attached,
initializeProjection: null,
out LiveEntityRecord? record);
Assert.NotNull(attached);
Assert.NotNull(record);
fixture.Controller.OnEntityReady(
LiveEntityReadyCandidate.Capture(record));
LiveEntityReadyCandidate.Capture(record!));
};
fixture.Controller.OnCreate(CelllessSpawn(
@ -1757,7 +1781,8 @@ public sealed class LiveEntityHydrationControllerTests
record,
staleVersion,
accepted));
Assert.Empty(fixture.Runtime.MaterializedWorldEntities);
Assert.Single(fixture.Runtime.MaterializedRecords);
Assert.Empty(fixture.Runtime.VisibleRecords);
}
[Fact]
@ -1890,6 +1915,17 @@ public sealed class LiveEntityHydrationControllerTests
}
}
public RuntimeEntityRecord Canonical
{
get
{
Assert.True(Runtime.TryGetCanonical(
Guid,
out RuntimeEntityRecord canonical));
return canonical;
}
}
public void Dispose()
{
try
@ -1991,24 +2027,24 @@ public sealed class LiveEntityHydrationControllerTests
public readonly List<(LiveProjectionPurpose Purpose, ushort Generation)> Calls = [];
public readonly List<ushort> PositionSequences = [];
public ulong? InstalledCreateIntegrationVersion { get; private set; }
public Action<LiveEntityRecord, WorldSession.EntitySpawn>? BeforeMaterialize { get; set; }
public Action<LiveEntityRecord, WorldSession.EntitySpawn>? AfterMaterialize { get; set; }
public Action<RuntimeEntityRecord, WorldSession.EntitySpawn>? BeforeMaterialize { get; set; }
public Action<RuntimeEntityRecord, WorldSession.EntitySpawn>? AfterMaterialize { get; set; }
public LiveProjectionPurpose? ThrowAfterMaterializePurposeOnce { get; set; }
public int SkipMaterializationCount { get; set; }
public bool TryMaterialize(
LiveEntityRecord expectedRecord,
RuntimeEntityRecord expectedCanonical,
WorldSession.EntitySpawn canonicalSpawn,
LiveProjectionPurpose purpose,
ulong expectedCreateIntegrationVersion,
AcDream.App.Rendering.LiveEntityAppearanceUpdateState? appearanceUpdate = null)
{
Calls.Add((purpose, expectedRecord.Generation));
Calls.Add((purpose, expectedCanonical.Generation));
PositionSequences.Add(canonicalSpawn.PositionSequence);
operations.Add($"materialize:{purpose}:{expectedRecord.Generation}");
BeforeMaterialize?.Invoke(expectedRecord, canonicalSpawn);
operations.Add($"materialize:{purpose}:{expectedCanonical.Generation}");
BeforeMaterialize?.Invoke(expectedCanonical, canonicalSpawn);
if (!runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCanonical,
expectedCreateIntegrationVersion))
{
return false;
@ -2030,7 +2066,7 @@ public sealed class LiveEntityHydrationControllerTests
}
WorldEntity? entity = runtime.MaterializeLiveEntity(
canonicalSpawn.Guid,
expectedCanonical,
position.LandblockId,
id => new WorldEntity
{
@ -2041,15 +2077,19 @@ public sealed class LiveEntityHydrationControllerTests
Rotation = Quaternion.Identity,
MeshRefs = [],
ParentCellId = position.LandblockId,
});
},
LiveEntityProjectionKind.World,
initializeProjection: null,
out LiveEntityRecord? expectedRecord);
if (ThrowAfterMaterializePurposeOnce == purpose)
{
ThrowAfterMaterializePurposeOnce = null;
throw new InvalidOperationException(
"fixture supersession projection failure");
}
AfterMaterialize?.Invoke(expectedRecord, canonicalSpawn);
AfterMaterialize?.Invoke(expectedCanonical, canonicalSpawn);
return entity is not null
&& expectedRecord is not null
&& runtime.IsCurrentRecord(expectedRecord);
}
@ -2137,7 +2177,7 @@ public sealed class LiveEntityHydrationControllerTests
{
}
public void OnProjectionRemoved(uint localEntityId)
public void OnProjectionRemoved(LiveEntityRecord record)
{
}