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

@ -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);
}