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:
parent
e18df84437
commit
420e5eea70
73 changed files with 2939 additions and 1715 deletions
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue