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

@ -6,6 +6,7 @@ using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Core.World;
namespace AcDream.App.Tests.World;
@ -16,7 +17,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000010u;
var runtime = Runtime();
LiveEntityRecord first = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord first = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
var firstMotion = new HostConsumerMotion();
runtime.SetRemoteMotionRuntime(guid, firstMotion);
EntityPhysicsHost firstHost = Host(guid);
@ -26,7 +27,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
Assert.True(runtime.TryGetPhysicsHost(guid, out IPhysicsObjHost resolved));
Assert.Same(firstHost, resolved);
LiveEntityRecord second = runtime.RegisterLiveEntity(Spawn(guid, 2)).Record!;
LiveEntityRecord second = runtime.RegisterAndMaterializeProjection(Spawn(guid, 2));
var secondMotion = new HostConsumerMotion();
runtime.SetRemoteMotionRuntime(guid, secondMotion);
EntityPhysicsHost replacement = Host(guid);
@ -42,7 +43,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000011u;
var runtime = Runtime();
LiveEntityRecord first = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord first = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
EntityPhysicsHost original = Host(guid);
runtime.InstallPhysicsHost(first, original);
@ -51,7 +52,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
Assert.Throws<InvalidOperationException>(() =>
runtime.InstallPhysicsHost(first, Host(guid)));
LiveEntityRecord second = runtime.RegisterLiveEntity(Spawn(guid, 2)).Record!;
LiveEntityRecord second = runtime.RegisterAndMaterializeProjection(Spawn(guid, 2));
Assert.Throws<InvalidOperationException>(() =>
runtime.InstallPhysicsHost(first, Host(guid)));
Assert.Null(second.PhysicsHost);
@ -63,8 +64,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
const uint watcherGuid = 0x70000020u;
const uint targetGuid = 0x70000021u;
var runtime = Runtime();
LiveEntityRecord watcherRecord = runtime.RegisterLiveEntity(Spawn(watcherGuid, 1)).Record!;
LiveEntityRecord targetRecord = runtime.RegisterLiveEntity(Spawn(targetGuid, 1)).Record!;
LiveEntityRecord watcherRecord = runtime.RegisterAndMaterializeProjection(Spawn(watcherGuid, 1));
LiveEntityRecord targetRecord = runtime.RegisterAndMaterializeProjection(Spawn(targetGuid, 1));
IPhysicsObjHost? Resolve(uint id) =>
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
? host
@ -129,7 +130,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000025u;
var runtime = Runtime();
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
EntityPhysicsHost original = Host(
guid,
position: new Vector3(1f, 0f, 0f));
@ -163,7 +164,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000022u;
var runtime = Runtime();
LiveEntityRecord first = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord first = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
int firstUpdates = 0;
int firstInterrupts = 0;
var firstHost = CallbackHost(
@ -172,7 +173,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
() => firstInterrupts++);
runtime.InstallPhysicsHost(first, firstHost);
LiveEntityRecord replacement = runtime.RegisterLiveEntity(Spawn(guid, 2)).Record!;
LiveEntityRecord replacement = runtime.RegisterAndMaterializeProjection(Spawn(guid, 2));
int replacementUpdates = 0;
int replacementInterrupts = 0;
var replacementHost = CallbackHost(
@ -195,7 +196,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000030u;
var runtime = Runtime();
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
var motion = new RemoteMotion(new PhysicsBody());
runtime.SetRemoteMotionRuntime(guid, motion);
EntityPhysicsHost minimal = Host(guid);
@ -221,7 +222,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000031u;
var runtime = Runtime();
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
var failing = new ThrowingCellConsumerMotion();
Assert.Throws<InvalidOperationException>(() =>
@ -240,11 +241,11 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000032u;
var runtime = Runtime();
runtime.RegisterLiveEntity(Spawn(guid, 1));
runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
var stale = new RemoteMotion(new PhysicsBody());
runtime.SetRemoteMotionRuntime(guid, stale);
LiveEntityRecord replacement = runtime.RegisterLiveEntity(Spawn(guid, 2)).Record!;
LiveEntityRecord replacement = runtime.RegisterAndMaterializeProjection(Spawn(guid, 2));
Assert.Throws<InvalidOperationException>(() =>
runtime.SetRemoteMotionRuntime(guid, stale));
Assert.Null(replacement.RemoteMotionRuntime);
@ -261,7 +262,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000033u;
var runtime = Runtime();
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
var firstBody = new PhysicsBody();
var secondBody = new PhysicsBody();
var alternating = new AlternatingBodyMotion(firstBody, secondBody);
@ -282,7 +283,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000036u;
var runtime = Runtime();
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
var canonical = new PhysicsBody();
var unrelated = new PhysicsBody();
PhysicsStateFlags unrelatedInitialState = unrelated.State;
@ -302,17 +303,18 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000034u;
var runtime = Runtime();
LiveEntityRecord retired = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord retired = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
var motion = new CallbackCanonicalMotion(() =>
runtime.RegisterLiveEntity(Spawn(guid, 2)));
Assert.Throws<InvalidOperationException>(() =>
runtime.SetRemoteMotionRuntime(guid, motion));
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord replacement));
Assert.Equal((ushort)2, replacement.Generation);
Assert.Null(replacement.RemoteMotionRuntime);
Assert.True(runtime.TryGetCanonical(guid, out var replacement));
Assert.Equal((ushort)2, replacement.Incarnation);
Assert.Null(replacement.LocalEntityId);
Assert.Null(replacement.PhysicsBody);
Assert.False(runtime.TryGetRecord(guid, out _));
Assert.Null(retired.RemoteMotionRuntime);
Assert.Null(retired.PhysicsBody);
Assert.False(runtime.TryGetRemoteMotionRuntime(guid, out _));
@ -323,7 +325,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000035u;
var runtime = Runtime();
LiveEntityRecord retired = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord retired = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
var motion = new CallbackCanonicalMotion(runtime.Clear);
Assert.Throws<InvalidOperationException>(() =>
@ -343,8 +345,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
const uint rightGuid = 0x70000041u;
LiveEntityRuntime runtime = null!;
runtime = Runtime(record => CleanPhysicsHost(record));
LiveEntityRecord leftRecord = runtime.RegisterLiveEntity(Spawn(leftGuid, 1)).Record!;
LiveEntityRecord rightRecord = runtime.RegisterLiveEntity(Spawn(rightGuid, 1)).Record!;
LiveEntityRecord leftRecord = runtime.RegisterAndMaterializeProjection(Spawn(leftGuid, 1));
LiveEntityRecord rightRecord = runtime.RegisterAndMaterializeProjection(Spawn(rightGuid, 1));
IPhysicsObjHost? Resolve(uint id) =>
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
? host
@ -374,8 +376,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
const uint rightGuid = 0x70000043u;
LiveEntityRuntime runtime = null!;
runtime = Runtime(record => CleanPhysicsHost(record));
LiveEntityRecord leftRecord = runtime.RegisterLiveEntity(Spawn(leftGuid, 1)).Record!;
LiveEntityRecord rightRecord = runtime.RegisterLiveEntity(Spawn(rightGuid, 1)).Record!;
LiveEntityRecord leftRecord = runtime.RegisterAndMaterializeProjection(Spawn(leftGuid, 1));
LiveEntityRecord rightRecord = runtime.RegisterAndMaterializeProjection(Spawn(rightGuid, 1));
IPhysicsObjHost? Resolve(uint id) =>
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
? host
@ -410,21 +412,14 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
if (failOnce)
{
failOnce = false;
replacementRecord = runtime.RegisterLiveEntity(Spawn(guid, 2)).Record!;
IPhysicsObjHost? Resolve(uint id) =>
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
? host
: null;
replacementHost = Host(guid, Resolve);
runtime.InstallPhysicsHost(replacementRecord, replacementHost);
replacementHost.PositionManager.StickTo(targetGuid, 0.5f, 1f);
runtime.RegisterLiveEntity(Spawn(guid, 2));
throw new InvalidOperationException("fixture failure before old host cleanup");
}
CleanPhysicsHost(record);
});
LiveEntityRecord oldRecord = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord targetRecord = runtime.RegisterLiveEntity(Spawn(targetGuid, 1)).Record!;
LiveEntityRecord oldRecord = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
LiveEntityRecord targetRecord = runtime.RegisterAndMaterializeProjection(Spawn(targetGuid, 1));
IPhysicsObjHost? InitialResolve(uint id) =>
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
? host
@ -438,6 +433,14 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
Assert.Throws<AggregateException>(() => runtime.UnregisterLiveEntity(
new DeleteObject.Parsed(guid, InstanceSequence: 1),
isLocalPlayer: false));
replacementRecord = runtime.RegisterAndMaterializeProjection(Spawn(guid, 2));
IPhysicsObjHost? ResolveReplacement(uint id) =>
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
? host
: null;
replacementHost = Host(guid, ResolveReplacement);
runtime.InstallPhysicsHost(replacementRecord, replacementHost);
replacementHost.PositionManager.StickTo(targetGuid, 0.5f, 1f);
Assert.Same(oldHost, oldRecord.PhysicsHost);
Assert.Same(replacementHost, replacementRecord!.PhysicsHost);
Assert.Equal(targetGuid, replacementHost!.PositionManager.GetStickyObjectId());
@ -466,8 +469,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
throw new InvalidOperationException("fixture post-exit failure");
}
});
LiveEntityRecord retiredRecord = runtime.RegisterLiveEntity(Spawn(retiredGuid, 1)).Record!;
LiveEntityRecord watcherRecord = runtime.RegisterLiveEntity(Spawn(watcherGuid, 1)).Record!;
LiveEntityRecord retiredRecord = runtime.RegisterAndMaterializeProjection(Spawn(retiredGuid, 1));
LiveEntityRecord watcherRecord = runtime.RegisterAndMaterializeProjection(Spawn(watcherGuid, 1));
IPhysicsObjHost? Resolve(uint id) =>
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
? host
@ -493,7 +496,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x70000048u;
var runtime = Runtime();
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
record.FullCellId = 0x01010123u;
record.WorldEntity = new AcDream.Core.World.WorldEntity
{
@ -521,32 +524,31 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
runtime = Runtime(record =>
{
if (record.ServerGuid == departingGuid)
{
LiveEntityRecord replacement = runtime.RegisterLiveEntity(
Spawn(departingGuid, 2)).Record!;
replacement.FullCellId = 0x02020202u;
replacement.WorldEntity = Entity(
departingGuid,
new Vector3(999f, 999f, 999f),
Quaternion.Identity);
runtime.InstallPhysicsHost(replacement, Host(departingGuid));
}
runtime.RegisterLiveEntity(Spawn(departingGuid, 2));
CleanPhysicsHost(record);
});
LiveEntityRecord departing = runtime.RegisterLiveEntity(Spawn(departingGuid, 1)).Record!;
LiveEntityRecord watcherRecord = runtime.RegisterLiveEntity(Spawn(watcherGuid, 1)).Record!;
departing.FullCellId = 0x01010123u;
Quaternion departingRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f);
departing.WorldEntity = Entity(
departingGuid,
new Vector3(12f, 34f, 56f),
departingRotation);
watcherRecord.WorldEntity = Entity(watcherGuid, Vector3.Zero, Quaternion.Identity);
LiveEntityRecord departing = runtime.RegisterAndMaterializeProjection(
Spawn(departingGuid, 1),
localId => Entity(
localId,
departingGuid,
new Vector3(12f, 34f, 56f),
departingRotation));
departing.FullCellId = 0x01010123u;
WorldEntity departingEntity = Assert.IsType<WorldEntity>(departing.WorldEntity);
LiveEntityRecord watcherRecord = runtime.RegisterAndMaterializeProjection(
Spawn(watcherGuid, 1),
localId => Entity(
localId,
watcherGuid,
Vector3.Zero,
Quaternion.Identity));
IPhysicsObjHost? Resolve(uint id) =>
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host) ? host : null;
var body = new PhysicsBody
{
Position = departing.WorldEntity.Position,
Position = departingEntity.Position,
Orientation = departingRotation,
};
var remote = new RemoteMotion(body);
@ -586,6 +588,14 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
Assert.True(runtime.UnregisterLiveEntity(
new DeleteObject.Parsed(departingGuid, InstanceSequence: 1),
isLocalPlayer: false));
LiveEntityRecord replacement = runtime.RegisterAndMaterializeProjection(
Spawn(departingGuid, 2));
replacement.FullCellId = 0x02020202u;
runtime.InstallPhysicsHost(
replacement,
Host(
departingGuid,
position: new Vector3(999f, 999f, 999f)));
TargetInfo exit = Assert.Single(delivered);
Assert.Equal(TargetStatus.ExitWorld, exit.Status);
@ -600,11 +610,11 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
{
const uint guid = 0x7000004Bu;
var runtime = Runtime();
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
EntityPhysicsHost host = EntityPhysicsHost.CreateMinimal(record, _ => null, () => 0);
runtime.InstallPhysicsHost(record, host);
Assert.Empty(runtime.WorldEntities);
Assert.Empty(runtime.VisibleRecords);
Assert.True(runtime.TryGetPhysicsHost(guid, out IPhysicsObjHost resolved));
Assert.Same(host, resolved);
}
@ -645,12 +655,13 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
InstanceSequence: instance);
private static AcDream.Core.World.WorldEntity Entity(
uint localId,
uint guid,
Vector3 position,
Quaternion rotation) =>
new()
{
Id = 1_000_000u + (guid & 0xFFFFu),
Id = localId,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = position,