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

@ -20,7 +20,7 @@ public sealed class AttachmentUpdateOrderTests
var calls = new List<uint>();
var children = insertionOrder.ToDictionary(id => id, id => parents[id]);
new AttachmentUpdateOrder<uint?>().ForEachParentFirst(
new AttachmentUpdateOrder<uint, uint?>().ForEachParentFirst(
children,
static parentId => parentId,
id =>
@ -44,7 +44,7 @@ public sealed class AttachmentUpdateOrderTests
};
var calls = new List<uint>();
IReadOnlyList<uint> failed = new AttachmentUpdateOrder<uint?>().ForEachParentFirst(
IReadOnlyList<uint> failed = new AttachmentUpdateOrder<uint, uint?>().ForEachParentFirst(
children,
static parentId => parentId,
id =>
@ -128,7 +128,7 @@ public sealed class AttachmentUpdateOrderTests
[20u] = 10u,
[30u] = 20u,
};
var order = new AttachmentUpdateOrder<uint?>().CollectSubtreePostOrder(
var order = new AttachmentUpdateOrder<uint, uint?>().CollectSubtreePostOrder(
children,
10u,
static parentId => parentId);
@ -146,7 +146,7 @@ public sealed class AttachmentUpdateOrderTests
};
var realized = new List<uint>();
new AttachmentUpdateOrder<uint?>().RealizeDescendants(
new AttachmentUpdateOrder<uint, uint?>().RealizeDescendants(
10u,
parent => waitingByParent.TryGetValue(parent, out var waiting)
? waiting
@ -170,7 +170,7 @@ public sealed class AttachmentUpdateOrderTests
};
var attempted = new List<uint>();
new AttachmentUpdateOrder<uint?>().RealizeDescendants(
new AttachmentUpdateOrder<uint, uint?>().RealizeDescendants(
10u,
parent => waitingByParent.TryGetValue(parent, out var waiting)
? waiting

View file

@ -106,8 +106,8 @@ public sealed class EquippedChildProjectionWithdrawalTests
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(parent, oldChild);
LiveEntityRecord replacement = fixture.Live.RegisterLiveEntity(
ControllerFixture.SpawnData(0x70000201u, generation: 2)).Record!;
LiveEntityRecord replacement = fixture.Live.RegisterAndMaterializeProjection(
ControllerFixture.SpawnData(0x70000201u, generation: 2));
Assert.Empty(fixture.Controller.AttachedEntityIds);
Assert.True(fixture.Live.TryGetRecord(0x70000201u, out LiveEntityRecord current));
@ -136,7 +136,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
fixture.Controller.ProjectionRemoved -= Throw;
Assert.Equal(1, fixture.Live.RetryPendingTeardowns());
static void Throw(uint _) =>
static void Throw(LiveEntityRecord _) =>
throw new InvalidOperationException("injected projection observer failure");
}
@ -424,11 +424,11 @@ public sealed class EquippedChildProjectionWithdrawalTests
fixture.Controller.OnParentEvent(new ParentEvent.Parsed(
invalidParent.ServerGuid, childGuid, 1, 0, 1, 2));
LiveEntityRecord child = fixture.RegisterOnly(
RuntimeEntityRecord childIdentity = fixture.RegisterOnly(
childGuid,
generation: 1,
hasPosition: true);
fixture.Materialize(child);
LiveEntityRecord child = fixture.Materialize(childIdentity);
Assert.True(fixture.Live.TryGetSnapshot(
childGuid,
out WorldSession.EntitySpawn childSpawn));
@ -455,7 +455,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
ExactProjectionWithdrawalDisposition.Completed,
Failure: null));
LiveEntityRecord parent = fixture.Spawn(0x70000254u, generation: 1);
LiveEntityRecord child = fixture.RegisterOnly(
RuntimeEntityRecord child = fixture.RegisterOnly(
0x70000255u,
generation: 1,
hasPosition: false);
@ -475,7 +475,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
out WorldSession.EntitySpawn snapshot));
Assert.Equal(parent.ServerGuid, snapshot.ParentGuid);
Assert.Null(snapshot.Position);
Assert.Null(child.WorldEntity);
Assert.False(fixture.Live.TryGetRecord(child.ServerGuid, out _));
var relation = new ParentAttachmentRelation(
parent.ServerGuid,
child.ServerGuid,
@ -490,9 +490,14 @@ public sealed class EquippedChildProjectionWithdrawalTests
Array.Empty<Matrix4x4>());
fixture.Controller.OnPosePublished(parent.ServerGuid);
Assert.NotNull(child.WorldEntity);
Assert.True(child.IsSpatiallyProjected);
Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind);
Assert.True(fixture.Live.TryGetRecord(
child.ServerGuid,
out LiveEntityRecord childProjection));
Assert.NotNull(childProjection.WorldEntity);
Assert.True(childProjection.IsSpatiallyProjected);
Assert.Equal(
LiveEntityProjectionKind.Attached,
childProjection.ProjectionKind);
}
[Fact]
@ -513,13 +518,12 @@ public sealed class EquippedChildProjectionWithdrawalTests
PlacementId = null,
PositionSequence = 0,
};
LiveEntityRecord child = fixture.Live.RegisterLiveEntity(childSpawn).Record!;
fixture.Live.RegisterLiveEntity(childSpawn);
fixture.Controller.OnSpawn(childSpawn);
var expected = new ParentAttachmentRelation(
parent.ServerGuid,
child.ServerGuid,
childSpawn.Guid,
ParentLocation: 0,
PlacementId: 0,
ParentInstanceSequence: 0,
@ -527,6 +531,9 @@ public sealed class EquippedChildProjectionWithdrawalTests
Assert.True(fixture.Live.ParentAttachments.IsCommitted(expected));
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
fixture.Controller.OnPosePublished(parent.ServerGuid);
Assert.True(fixture.Live.TryGetRecord(
childSpawn.Guid,
out LiveEntityRecord child));
Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind);
Assert.NotNull(child.WorldEntity);
}
@ -560,10 +567,13 @@ public sealed class EquippedChildProjectionWithdrawalTests
PlacementId = 0,
PositionSequence = 0,
};
LiveEntityRecord child = fixture.Live.RegisterLiveEntity(childSpawn).Record!;
fixture.Live.RegisterLiveEntity(childSpawn);
fixture.Controller.OnSpawn(childSpawn);
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
fixture.Controller.OnPosePublished(parent.ServerGuid);
Assert.True(fixture.Live.TryGetRecord(
childSpawn.Guid,
out LiveEntityRecord child));
WorldEntity entity = child.WorldEntity!;
Assert.Null(entity.PaletteOverride);
WorldSession.EntitySpawn grandchildSpawn = ControllerFixture.SpawnData(
@ -576,9 +586,11 @@ public sealed class EquippedChildProjectionWithdrawalTests
PlacementId = 0,
PositionSequence = 0,
};
LiveEntityRecord grandchild = fixture.Live.RegisterLiveEntity(
grandchildSpawn).Record!;
fixture.Live.RegisterLiveEntity(grandchildSpawn);
fixture.Controller.OnSpawn(grandchildSpawn);
Assert.True(fixture.Live.TryGetRecord(
grandchildSpawn.Guid,
out LiveEntityRecord grandchild));
WorldEntity grandchildEntity = grandchild.WorldEntity!;
Assert.Equal(
LiveEntityProjectionKind.Attached,
@ -642,12 +654,12 @@ public sealed class EquippedChildProjectionWithdrawalTests
using (fixture)
{
LiveEntityRecord parent = fixture.Spawn(0x7000025Cu, generation: 1);
LiveEntityRecord child = fixture.RegisterOnly(
RuntimeEntityRecord childIdentity = fixture.RegisterOnly(
0x7000025Du,
generation: 1,
hasPosition: true,
hasSetup: false);
fixture.Materialize(child);
LiveEntityRecord child = fixture.Materialize(childIdentity);
child.HasPartArray = false;
var update = new ParentEvent.Parsed(
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1);
@ -763,7 +775,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
0x70000257u,
generation: 1,
LiveEntityProjectionKind.Attached);
LiveEntityRecord waitingParent = fixture.RegisterOnly(
RuntimeEntityRecord waitingParent = fixture.RegisterOnly(
0x70000258u,
generation: 1,
hasPosition: true);
@ -1117,7 +1129,8 @@ public sealed class EquippedChildProjectionWithdrawalTests
}
}
private static LiveEntityRecord ChildRecord() => new(
private static LiveEntityRecord ChildRecord() =>
LiveEntityTestFixture.CreateExactProjectionRecord(
new WorldSession.EntitySpawn(
0x70000100u,
new CreateObject.ServerPosition(
@ -1188,12 +1201,12 @@ public sealed class EquippedChildProjectionWithdrawalTests
ushort generation,
LiveEntityProjectionKind kind = LiveEntityProjectionKind.World)
{
LiveEntityRecord record = RegisterOnly(guid, generation, hasPosition: true);
Materialize(record, kind);
return record;
RuntimeEntityRecord canonical =
RegisterOnly(guid, generation, hasPosition: true);
return Materialize(canonical, kind);
}
internal LiveEntityRecord RegisterOnly(
internal RuntimeEntityRecord RegisterOnly(
uint guid,
ushort generation,
bool hasPosition,
@ -1204,28 +1217,35 @@ public sealed class EquippedChildProjectionWithdrawalTests
spawn = spawn with { Position = null };
if (!hasSetup)
spawn = spawn with { SetupTableId = null };
return Live.RegisterLiveEntity(spawn).Record!;
return Assert.IsType<RuntimeEntityRecord>(
Live.RegisterLiveEntity(spawn).Canonical);
}
internal void Materialize(
LiveEntityRecord record,
internal LiveEntityRecord Materialize(
RuntimeEntityRecord canonical,
LiveEntityProjectionKind kind = LiveEntityProjectionKind.World)
{
Live.MaterializeLiveEntity(
record.ServerGuid,
WorldEntity? entity = Live.MaterializeLiveEntity(
canonical,
Cell,
id => new WorldEntity
{
Id = id,
ServerGuid = record.ServerGuid,
ServerGuid = canonical.ServerGuid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = Cell,
},
kind);
kind,
initializeProjection: null,
out LiveEntityRecord? projected);
Assert.NotNull(entity);
LiveEntityRecord record =
Assert.IsType<LiveEntityRecord>(projected);
record.HasPartArray = true;
return record;
}
internal static WorldSession.EntitySpawn SpawnData(uint guid, ushort generation) => new(
@ -1277,7 +1297,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
"_attachedByChild",
BindingFlags.Instance | BindingFlags.NonPublic)!;
var map = (IDictionary)mapField.GetValue(Controller)!;
map.Add(child.ServerGuid, attached);
map.Add(child.ProjectionKey!.Value, attached);
}
internal void CommitRenderedRelation(ParentAttachmentRelation relation)

View file

@ -20,7 +20,7 @@ public sealed class LiveAppearanceAnimationTests
var runtime = new AcDream.App.World.LiveEntityRuntime(
new AcDream.App.Streaming.GpuWorldState(),
new AcDream.App.World.DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
var record = runtime.RegisterLiveEntity(new AcDream.Core.Net.WorldSession.EntitySpawn(
var spawn = new AcDream.Core.Net.WorldSession.EntitySpawn(
guid,
Position: null,
SetupTableId: null,
@ -33,8 +33,11 @@ public sealed class LiveAppearanceAnimationTests
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: 1)).Record!;
WorldEntity entity = Entity(0x70000002u, 0x01000001u);
InstanceSequence: 1);
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(
spawn,
id => Entity(id, 0x01000001u, guid));
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
var state = new LiveEntityAnimationState
{
Entity = entity,
@ -47,7 +50,6 @@ public sealed class LiveAppearanceAnimationTests
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
PartAvailability = Array.Empty<bool>(),
};
record.WorldEntity = entity;
record.AnimationRuntime = state;
LiveEntityAppearanceUpdateState captured = Assert.IsType<LiveEntityAppearanceUpdateState>(
@ -188,10 +190,13 @@ public sealed class LiveAppearanceAnimationTests
MotionTableId: null,
InstanceSequence: instance);
private static WorldEntity Entity(uint id, uint gfxObjId) => new()
private static WorldEntity Entity(
uint id,
uint gfxObjId,
uint serverGuid = 0x50000001u) => new()
{
Id = id,
ServerGuid = 0x50000001u,
ServerGuid = serverGuid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,

View file

@ -7,6 +7,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;
using DatReaderWriter.Types;
@ -64,9 +65,9 @@ public sealed class LiveEntityAnimationPresenterTests
old.Live.SetAnimationRuntime(Guid, replacement);
var presenter = Presenter(old.Live, new EntityEffectPoseRegistry(), new Context());
presenter.Present(new Dictionary<uint, LiveEntityAnimationSchedule>
presenter.Present(new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
{
[old.Entity.Id] = stale,
[old.Record.ProjectionKey!.Value] = stale,
});
Assert.Empty(replacement.MeshRefsScratch);
@ -178,9 +179,9 @@ public sealed class LiveEntityAnimationPresenterTests
[true]);
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), new Context());
presenter.Present(new Dictionary<uint, LiveEntityAnimationSchedule>
presenter.Present(new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
{
[fixture.Entity.Id] = stale,
[fixture.Record.ProjectionKey!.Value] = stale,
});
Assert.Empty(fixture.State.MeshRefsScratch);
@ -252,11 +253,11 @@ public sealed class LiveEntityAnimationPresenterTests
Assert.Equal(2, nested.Count);
};
var presenter = Presenter(first.Live, poses, new Context());
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
var schedules = new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
{
[first.Entity.Id] = Schedule(first,
[first.Record.ProjectionKey!.Value] = Schedule(first,
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
[second.Entity.Id] = Schedule(second,
[second.Record.ProjectionKey!.Value] = Schedule(second,
[new PartTransform(Vector3.UnitY, Quaternion.Identity)]),
};
@ -283,11 +284,11 @@ public sealed class LiveEntityAnimationPresenterTests
first.Live.SetAnimationRuntime(Guid + 1, replacement);
};
var presenter = Presenter(first.Live, poses, new Context());
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
var schedules = new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
{
[first.Entity.Id] = Schedule(first,
[first.Record.ProjectionKey!.Value] = Schedule(first,
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
[second.Entity.Id] = Schedule(second,
[second.Record.ProjectionKey!.Value] = Schedule(second,
[new PartTransform(new Vector3(99f, 0f, 0f), Quaternion.Identity)]),
};
@ -305,11 +306,11 @@ public sealed class LiveEntityAnimationPresenterTests
var second = Add(first.Live, Guid + 1, partCount: 1);
var poses = new EntityEffectPoseRegistry();
var presenter = Presenter(first.Live, poses, new Context());
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
var schedules = new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
{
[first.Entity.Id] = Schedule(first,
[first.Record.ProjectionKey!.Value] = Schedule(first,
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
[second.Entity.Id] = Schedule(second,
[second.Record.ProjectionKey!.Value] = Schedule(second,
[new PartTransform(Vector3.UnitY, Quaternion.Identity)]),
};
bool recursed = false;
@ -359,9 +360,9 @@ public sealed class LiveEntityAnimationPresenterTests
fixture.Record.ObjectClockEpoch,
fixture.Record.ProjectionMutationVersion,
fixture.State.PresentationRevision);
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
var schedules = new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
{
[fixture.Entity.Id] = schedule,
[fixture.Record.ProjectionKey!.Value] = schedule,
};
var poses = new EntityEffectPoseRegistry();
var presenter = Presenter(fixture.Live, poses, new Context());
@ -407,12 +408,12 @@ public sealed class LiveEntityAnimationPresenterTests
}
}
private static IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> Schedules(
private static IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> Schedules(
Fixture fixture,
IReadOnlyList<PartTransform> frames) =>
new Dictionary<uint, LiveEntityAnimationSchedule>
new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
{
[fixture.Entity.Id] = Schedule(fixture, frames),
[fixture.Record.ProjectionKey!.Value] = Schedule(fixture, frames),
};
private static LiveEntityAnimationSchedule Schedule(
@ -452,10 +453,8 @@ public sealed class LiveEntityAnimationPresenterTests
float scale = 1f,
bool withSequencer = true)
{
LiveEntityRecord record = live.RegisterLiveEntity(Spawn(guid)).Record!;
WorldEntity entity = live.MaterializeLiveEntity(
guid,
Cell,
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
Spawn(guid),
id => new WorldEntity
{
Id = id,
@ -465,7 +464,8 @@ public sealed class LiveEntityAnimationPresenterTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = Cell,
})!;
});
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
LiveEntityAnimationState state = State(entity, partCount, scale, withSequencer);
entity.SetIndexedPartPoses(
Enumerable.Repeat(Matrix4x4.Identity, partCount).ToArray(),

View file

@ -10,6 +10,7 @@ using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using AcDream.Core.Vfx;
using AcDream.Runtime.Entities;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
@ -36,14 +37,14 @@ public sealed class LiveEntityAnimationSchedulerTests
localHiddenPartPoseDirty: true,
liveCenterX: 0,
liveCenterY: 0)
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule marked));
.TryGetValue(record.ProjectionKey!.Value, out LiveEntityAnimationSchedule marked));
bool repeated = scheduler.Tick(
PhysicsBody.MaxQuantum,
animation.Entity.Position,
localHiddenPartPoseDirty: false,
liveCenterX: 0,
liveCenterY: 0)
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule consumed);
.TryGetValue(record.ProjectionKey!.Value, out LiveEntityAnimationSchedule consumed);
Assert.True(marked.ComposeParts);
Assert.NotNull(marked.SequenceFrames);
@ -73,14 +74,14 @@ public sealed class LiveEntityAnimationSchedulerTests
localHiddenPartPoseDirty: false,
liveCenterX: 0,
liveCenterY: 0)
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule marked));
.TryGetValue(record.ProjectionKey!.Value, out LiveEntityAnimationSchedule marked));
bool repeated = scheduler.Tick(
elapsedSeconds: 0f,
playerPosition: null,
localHiddenPartPoseDirty: false,
liveCenterX: 0,
liveCenterY: 0)
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule consumed);
.TryGetValue(record.ProjectionKey!.Value, out LiveEntityAnimationSchedule consumed);
Assert.True(marked.ComposeParts);
Assert.NotNull(marked.SequenceFrames);
@ -91,7 +92,7 @@ public sealed class LiveEntityAnimationSchedulerTests
[Fact]
public void VisibleAnimationWithoutRemoteMotion_AppliesCompleteRootFrame()
{
var (live, _, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
var retainedBodyOwner = BuildRemote(animation.Entity);
retainedBodyOwner.Body.SnapToCell(
Cell,
@ -108,7 +109,7 @@ public sealed class LiveEntityAnimationSchedulerTests
localHiddenPartPoseDirty: false,
liveCenterX: 0,
liveCenterY: 0)
.ContainsKey(animation.Entity.Id));
.ContainsKey(record.ProjectionKey!.Value));
Assert.True(animation.Entity.Position.X > startX + 1f);
}
@ -116,14 +117,14 @@ public sealed class LiveEntityAnimationSchedulerTests
[Fact]
public void ScheduleOwnsPartFramesAcrossLaterSequencerSampling()
{
var (live, _, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid);
LiveEntityAnimationSchedule schedule = scheduler.Tick(
0.1f,
animation.Entity.Position,
localHiddenPartPoseDirty: false,
liveCenterX: 0,
liveCenterY: 0)[animation.Entity.Id];
liveCenterY: 0)[record.ProjectionKey!.Value];
PartTransform retained = schedule.SequenceFrames![0];
animation.CaptureSequenceFrames(
[new PartTransform(new Vector3(77f, 76f, 75f), Quaternion.Identity)]);
@ -194,7 +195,7 @@ public sealed class LiveEntityAnimationSchedulerTests
liveCenterY: 0);
Assert.True(entity.Position.X > startX);
Assert.Same(record, live.SpatialRootObjects[RemoteGuid]);
Assert.True(live.IsCurrentSpatialRootObject(record));
}
[Fact]
@ -224,7 +225,7 @@ public sealed class LiveEntityAnimationSchedulerTests
Assert.True(entity.Position.X > startX + 0.5f);
Assert.True(remote.Interp.IsActive || entity.Position.X >= startX + 0.99f);
Assert.Same(record, live.SpatialRootObjects[RemoteGuid]);
Assert.True(live.IsCurrentSpatialRootObject(record));
}
[Fact]
@ -343,14 +344,14 @@ public sealed class LiveEntityAnimationSchedulerTests
physics: physics,
projectiles: projectiles);
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
0.1f,
animation.Entity.Position,
localHiddenPartPoseDirty: false,
liveCenterX: 1,
liveCenterY: 1);
Assert.Contains(animation.Entity.Id, schedules.Keys);
Assert.Contains(record.ProjectionKey!.Value, schedules.Keys);
Assert.Equal(1, rootPublishes);
float distance = animation.Entity.Position.X - startX;
Assert.InRange(distance, 0.79f, 0.81f);
@ -374,7 +375,7 @@ public sealed class LiveEntityAnimationSchedulerTests
isLocalPlayer: false);
});
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
PhysicsBody.MaxQuantum * 2.5f,
animation.Entity.Position,
localHiddenPartPoseDirty: false,
@ -407,7 +408,7 @@ public sealed class LiveEntityAnimationSchedulerTests
},
_ => rootPublishes++);
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
PhysicsBody.MaxQuantum * 2.5f,
animation.Entity.Position,
localHiddenPartPoseDirty: false,
@ -441,7 +442,7 @@ public sealed class LiveEntityAnimationSchedulerTests
},
_ => rootPublishes++);
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
PhysicsBody.MaxQuantum * 2.5f,
animation.Entity.Position,
localHiddenPartPoseDirty: false,
@ -472,7 +473,7 @@ public sealed class LiveEntityAnimationSchedulerTests
},
_ => rootPublishes++);
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
PhysicsBody.MaxQuantum * 2.5f,
animation.Entity.Position,
localHiddenPartPoseDirty: false,
@ -509,7 +510,7 @@ public sealed class LiveEntityAnimationSchedulerTests
},
_ => rootPublishes++);
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
PhysicsBody.MaxQuantum * 2.5f,
animation.Entity.Position,
localHiddenPartPoseDirty: false,
@ -572,7 +573,7 @@ public sealed class LiveEntityAnimationSchedulerTests
_ => throw new InvalidOperationException(),
LiveEntityProjectionKind.Attached));
Assert.False(record.ObjectClock.IsActive);
Assert.DoesNotContain(RemoteGuid, live.SpatialRootObjects.Keys);
Assert.False(live.IsCurrentSpatialRootObject(record));
scheduler.Tick(
PhysicsBody.MaxQuantum,
@ -590,7 +591,7 @@ public sealed class LiveEntityAnimationSchedulerTests
_ => throw new InvalidOperationException(),
LiveEntityProjectionKind.World));
Assert.True(record.ObjectClock.IsActive);
Assert.Contains(RemoteGuid, live.SpatialRootObjects.Keys);
Assert.True(live.IsCurrentSpatialRootObject(record));
scheduler.Tick(
PhysicsBody.MaxQuantum,
@ -634,8 +635,8 @@ public sealed class LiveEntityAnimationSchedulerTests
(_, _) => (0.48f, 1.835f));
var identity = new LocalPlayerIdentityState { ServerGuid = localPlayerGuid };
var poses = new EntityEffectPoseRegistry();
foreach (WorldEntity entity in live.MaterializedWorldEntities.Values)
poses.PublishMeshRefs(entity);
foreach (LiveEntityRecord record in live.MaterializedRecords)
poses.PublishMeshRefs(record.WorldEntity!);
IAnimationHookCaptureSink animationHooks = captureHooks is null
? new AnimationHookCaptureSink(new AnimationHookFrameQueue(
new AnimationHookRouter(),
@ -681,10 +682,8 @@ public sealed class LiveEntityAnimationSchedulerTests
var live = new LiveEntityRuntime(
spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
LiveEntityRecord record = live.RegisterLiveEntity(Spawn(guid)).Record!;
WorldEntity entity = live.MaterializeLiveEntity(
guid,
Cell,
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
Spawn(guid),
id => new WorldEntity
{
Id = id,
@ -694,7 +693,8 @@ public sealed class LiveEntityAnimationSchedulerTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = Cell,
})!;
});
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
var setup = new Setup();
var animation = new LiveEntityAnimationState
{
@ -803,10 +803,8 @@ public sealed class LiveEntityAnimationSchedulerTests
var live = new LiveEntityRuntime(
spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
LiveEntityRecord record = live.RegisterLiveEntity(Spawn(guid, state)).Record!;
WorldEntity entity = live.MaterializeLiveEntity(
guid,
Cell,
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
Spawn(guid, state),
id => new WorldEntity
{
Id = id,
@ -816,7 +814,8 @@ public sealed class LiveEntityAnimationSchedulerTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = Cell,
})!;
});
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
return (live, record, entity);
}

View file

@ -294,10 +294,8 @@ public sealed class LiveEntityCreateSupersessionRecoveryTests
private static LiveEntityRecord RegisterAndMaterialize(LiveEntityRuntime runtime)
{
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn()).Record!;
runtime.MaterializeLiveEntity(
Guid,
Cell,
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(
Spawn(),
id => new WorldEntity
{
Id = id,

View file

@ -147,8 +147,8 @@ public sealed class LiveRenderProjectionJournalTests
incarnation,
harness.Journal.Pending[0].Record.OwnerIncarnation);
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
harness.Projections.OnProjectionRemoved(record);
harness.Projections.OnProjectionRemoved(record);
Assert.Equal(2, harness.Journal.Count);
Assert.Equal(
RenderProjectionDeltaKind.UpdateFlags,
@ -166,13 +166,10 @@ public sealed class LiveRenderProjectionJournalTests
uint firstLocalId = first.WorldEntity!.Id;
harness.Journal.DrainTo(harness.Scene);
LiveEntityRegistrationResult replacement =
harness.Runtime.RegisterLiveEntity(Spawn(Guid, instance: 5));
LiveEntityRecord second = replacement.Record!;
WorldEntity secondEntity = harness.Runtime.MaterializeLiveEntity(
Guid,
CellId,
localId => Entity(localId, Guid))!;
LiveEntityRecord second = harness.Runtime.RegisterAndMaterializeProjection(
Spawn(Guid, instance: 5),
localId => Entity(localId, Guid));
WorldEntity secondEntity = Assert.IsType<WorldEntity>(second.WorldEntity);
second.InitialHydrationCompleted = true;
LiveEntityReadyCandidate current = LiveEntityReadyCandidate.Capture(second);
Assert.True(harness.Projections.OnEntityReady(current));
@ -209,7 +206,7 @@ public sealed class LiveRenderProjectionJournalTests
LiveEntityRegistrationResult duplicate =
harness.Runtime.RegisterLiveEntity(Spawn(Guid, instance: 5));
Assert.Same(record, duplicate.Record);
Assert.Same(record, duplicate.Projection);
Assert.True(harness.Projections.OnEntityReady(
LiveEntityReadyCandidate.Capture(record)));
@ -285,7 +282,7 @@ public sealed class LiveRenderProjectionJournalTests
LiveEntityReadyCandidate.Capture(record)));
harness.Journal.DrainTo(harness.Scene);
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
harness.Projections.OnProjectionRemoved(record);
harness.Journal.DrainTo(harness.Scene);
harness.Projections.SynchronizeActiveSources();
@ -311,7 +308,7 @@ public sealed class LiveRenderProjectionJournalTests
LiveEntityReadyCandidate.Capture(record)));
harness.Journal.DrainTo(harness.Scene);
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
harness.Projections.OnProjectionRemoved(record);
harness.Journal.DrainTo(harness.Scene);
Assert.True(harness.Runtime.RebucketLiveEntity(Guid, CellId));
@ -337,7 +334,7 @@ public sealed class LiveRenderProjectionJournalTests
LiveEntityReadyCandidate.Capture(record)));
harness.Journal.DrainTo(harness.Scene);
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
harness.Projections.OnProjectionRemoved(record);
harness.Journal.DrainTo(harness.Scene);
Assert.True(harness.Runtime.UnregisterLiveEntity(
@ -383,14 +380,11 @@ public sealed class LiveRenderProjectionJournalTests
LiveEntityProjectionKind projectionKind =
LiveEntityProjectionKind.World)
{
LiveEntityRecord record =
harness.Runtime.RegisterLiveEntity(Spawn(guid, instance)).Record!;
WorldEntity? entity = harness.Runtime.MaterializeLiveEntity(
guid,
CellId,
LiveEntityRecord record = harness.Runtime.RegisterAndMaterializeProjection(
Spawn(guid, instance),
localId => Entity(localId, guid),
projectionKind);
Assert.NotNull(entity);
Assert.NotNull(record.WorldEntity);
record.InitialHydrationCompleted = true;
return record;
}

View file

@ -635,11 +635,10 @@ public sealed class RetailStaticAnimatingObjectSchedulerTests
body.SnapToCell(0x01010001u, entity.Position, Vector3.Zero);
LiveEntityAnimationState state = LiveState(entity, setup, sequencer);
Assert.True(scheduler.BindLiveOwner(entity, state, body));
var record = new LiveEntityRecord(Spawn(serverGuid))
{
WorldEntity = entity,
AnimationRuntime = state,
};
var record = LiveEntityTestFixture.CreateExactProjectionRecord(
Spawn(serverGuid));
record.WorldEntity = entity;
record.AnimationRuntime = state;
int motionDone = 0;
sequencer.MotionDoneTarget = (_, success) =>
{

View file

@ -119,12 +119,12 @@ public sealed class EntityEffectControllerTests
EntityEffectProfile? profile = null)
{
WorldSession.EntitySpawn spawn = Spawn(guid, generation);
Runtime.RegisterLiveEntity(spawn);
Runtime.SetEffectProfile(guid, profile ?? LiveProfile());
WorldEntity entity = Runtime.MaterializeLiveEntity(
guid,
spawn.Position!.Value.LandblockId,
id => Entity(id, guid))!;
LiveEntityRecord record = Runtime.RegisterAndMaterializeProjection(
spawn,
id => Entity(id, guid),
initializeProjection: exact =>
exact.EffectProfile = profile ?? LiveProfile());
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
Assert.True(Controller.OnLiveEntityReady(guid));
return entity;
}
@ -172,12 +172,11 @@ public sealed class EntityEffectControllerTests
var fixture = new Fixture();
fixture.Controller.HandleDirect(new PlayPhysicsScript(Guid, DirectDid));
WorldSession.EntitySpawn spawn = Spawn(Guid, 1);
fixture.Runtime.RegisterLiveEntity(spawn);
fixture.Runtime.SetEffectProfile(Guid, Fixture.LiveProfile());
fixture.Runtime.MaterializeLiveEntity(
Guid,
spawn.Position!.Value.LandblockId,
id => Entity(id, Guid));
fixture.Runtime.RegisterAndMaterializeProjection(
spawn,
id => Entity(id, Guid),
initializeProjection: record =>
record.EffectProfile = Fixture.LiveProfile());
Assert.True(fixture.Controller.PrepareLiveEntityOwner(Guid));
Assert.Equal(1, fixture.Controller.PendingPacketCount);
@ -279,12 +278,13 @@ public sealed class EntityEffectControllerTests
var fixture = new Fixture();
fixture.Controller.HandleDirect(new PlayPhysicsScript(Guid, DirectDid));
WorldSession.EntitySpawn spawn = Spawn(Guid, 1);
fixture.Runtime.RegisterLiveEntity(spawn);
fixture.Runtime.SetEffectProfile(Guid, Fixture.LiveProfile());
WorldEntity entity = fixture.Runtime.MaterializeLiveEntity(
Guid,
0x02020001u,
id => Entity(id, Guid))!;
LiveEntityRecord record = fixture.Runtime.RegisterAndMaterializeProjection(
spawn,
id => Entity(id, Guid),
initializeProjection: exact =>
exact.EffectProfile = Fixture.LiveProfile());
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
fixture.Runtime.RebucketLiveEntity(Guid, 0x02020001u);
Assert.True(fixture.Controller.OnLiveEntityReady(Guid));
Assert.Equal(1, fixture.Controller.PendingPacketCount);
@ -360,6 +360,7 @@ public sealed class EntityEffectControllerTests
WorldSession.EntitySpawn first = Spawn(Guid, generation: 1);
fixture.Runtime.RegisterLiveEntity(first);
fixture.Controller.ForgetUnknownOwner(Guid);
Assert.True(fixture.Runtime.UnregisterLiveEntity(
new DeleteObject.Parsed(Guid, 1),
isLocalPlayer: false));
@ -383,11 +384,11 @@ public sealed class EntityEffectControllerTests
isLocalPlayer: false));
Assert.Equal(1, fixture.Controller.PendingPacketCount);
fixture.Runtime.SetEffectProfile(Guid, Fixture.LiveProfile());
fixture.Runtime.MaterializeLiveEntity(
Guid,
current.Position!.Value.LandblockId,
id => Entity(id, Guid));
fixture.Runtime.RegisterAndMaterializeProjection(
current,
id => Entity(id, Guid),
initializeProjection: record =>
record.EffectProfile = Fixture.LiveProfile());
Assert.True(fixture.Controller.OnLiveEntityReady(Guid));
fixture.Runner.Tick(0.0);
Assert.Equal([1u], EmitterIds(fixture.Sink));

View file

@ -150,7 +150,7 @@ public sealed class EntitySpawnAdapterLifetimeTests
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
WorldEntity oldEntity = MakeEntity(61u, guid);
oldEntity.MeshRefs = [new MeshRef(0x01000100u, Matrix4x4.Identity)];
WorldEntity replacement = MakeEntity(62u, guid);
WorldEntity replacement = MakeEntity(61u, guid);
replacement.MeshRefs = [new MeshRef(0x01000200u, Matrix4x4.Identity)];
adapter.OnCreate(oldEntity);
@ -280,7 +280,7 @@ public sealed class EntitySpawnAdapterLifetimeTests
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
WorldEntity oldEntity = MakeEntity(101u, guid);
oldEntity.MeshRefs = [new MeshRef((uint)oldMeshId, Matrix4x4.Identity)];
WorldEntity replacement = MakeEntity(102u, guid);
WorldEntity replacement = MakeEntity(101u, guid);
AnimatedEntityState oldState = Assert.IsType<AnimatedEntityState>(adapter.OnCreate(oldEntity));
Assert.True(adapter.SetPresentationResident(oldEntity, resident: true));
@ -682,7 +682,7 @@ public sealed class EntitySpawnAdapterLifetimeTests
adapter.OnCreate(oldEntity);
Assert.True(adapter.SetPresentationResident(oldEntity, resident: true));
WorldEntity replacement = MakeEntity(202u, guid);
WorldEntity replacement = MakeEntity(201u, guid);
replacement.MeshRefs = [new MeshRef((uint)replacementMesh, Matrix4x4.Identity)];
adapter.OnCreate(replacement);
Assert.True(adapter.SetPresentationResident(replacement, resident: true));