refactor(runtime): own canonical entity and object lifetime

Introduce one presentation-free RuntimeEntityObjectLifetime for the exact entity directory and ClientObjectTable. Make GameWindow, graphical projections, retained UI, interaction, session routing, create/delete integration, and reset borrow that owner while preserving synchronous retail ordering, dormant retention, and retry semantics.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 05:54:46 +02:00
parent d9bf4c4960
commit 5ef8b5371d
40 changed files with 712 additions and 170 deletions

View file

@ -0,0 +1,251 @@
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
namespace AcDream.Runtime.Tests.Entities;
public sealed class RuntimeEntityObjectLifetimeTests
{
[Fact]
public void Owner_ConstructsOneExactDirectoryAndObjectTablePerInstance()
{
var first = new RuntimeEntityObjectLifetime();
var second = new RuntimeEntityObjectLifetime();
Assert.NotSame(first.Entities, second.Entities);
Assert.NotSame(first.Objects, second.Objects);
Assert.Same(first.Entities, first.Entities);
Assert.Same(first.Objects, first.Objects);
}
[Fact]
public void Owner_ConstructionLoadsNoPresentationOrBackendAssembly()
{
_ = new RuntimeEntityObjectLifetime();
string[] loaded = AppDomain.CurrentDomain
.GetAssemblies()
.Select(assembly => assembly.GetName().Name ?? string.Empty)
.ToArray();
Assert.DoesNotContain(loaded, name =>
name.StartsWith("AcDream.App", StringComparison.Ordinal)
|| name.StartsWith("AcDream.UI.", StringComparison.Ordinal)
|| name.StartsWith("Silk.NET", StringComparison.Ordinal)
|| name.StartsWith("OpenAL", StringComparison.Ordinal)
|| name.StartsWith("ImGui", StringComparison.Ordinal));
}
[Fact]
public void SeparateOwners_IsolateConflictingGuidsAndContents()
{
const uint guid = 0x70000001u;
var first = new RuntimeEntityObjectLifetime();
var second = new RuntimeEntityObjectLifetime();
RuntimeEntityRecord firstRecord = Accept(first, Spawn(guid, 1, "first"));
RuntimeEntityRecord secondRecord = Accept(second, Spawn(guid, 9, "second"));
Assert.True(first.ApplyAcceptedSpawn(
firstRecord,
firstRecord.CreateIntegrationVersion,
firstRecord.Snapshot,
replaceGeneration: false));
Assert.True(second.ApplyAcceptedSpawn(
secondRecord,
secondRecord.CreateIntegrationVersion,
secondRecord.Snapshot,
replaceGeneration: false));
Assert.Equal("first", first.Objects.Get(guid)!.Name);
Assert.Equal("second", second.Objects.Get(guid)!.Name);
Assert.Equal((ushort)1, firstRecord.Incarnation);
Assert.Equal((ushort)9, secondRecord.Incarnation);
}
[Fact]
public void AcceptedSpawn_RejectsWrongOrSupersededCanonicalIncarnation()
{
var lifetime = new RuntimeEntityObjectLifetime();
WorldSession.EntitySpawn spawn = Spawn(0x70000002u, 3, "current");
RuntimeEntityRecord canonical = Accept(lifetime, spawn);
var wrong = new RuntimeEntityObjectLifetime();
RuntimeEntityRecord wrongCanonical = Accept(wrong, spawn);
Assert.False(lifetime.ApplyAcceptedSpawn(
wrongCanonical,
wrongCanonical.CreateIntegrationVersion,
spawn,
replaceGeneration: false));
ulong expectedVersion = canonical.CreateIntegrationVersion;
lifetime.Entities.AdvanceCreateAuthority(canonical);
Assert.False(lifetime.ApplyAcceptedSpawn(
canonical,
expectedVersion,
spawn,
replaceGeneration: false));
Assert.Null(lifetime.Objects.Get(spawn.Guid));
}
[Fact]
public void AcceptedSpawn_RevalidatesAfterSynchronousObjectCallbacks()
{
var lifetime = new RuntimeEntityObjectLifetime();
WorldSession.EntitySpawn spawn = Spawn(0x70000003u, 4, "reentrant");
RuntimeEntityRecord canonical = Accept(lifetime, spawn);
ulong expectedVersion = canonical.CreateIntegrationVersion;
lifetime.Objects.ObjectAdded += _ =>
lifetime.Entities.AdvanceCreateAuthority(canonical);
Assert.False(lifetime.ApplyAcceptedSpawn(
canonical,
expectedVersion,
spawn,
replaceGeneration: false));
Assert.NotNull(lifetime.Objects.Get(spawn.Guid));
}
[Fact]
public void AcceptedDelete_RemovesOnlyMatchingLogicalGeneration()
{
var lifetime = new RuntimeEntityObjectLifetime();
WorldSession.EntitySpawn spawn = Spawn(0x70000004u, 6, "retained");
RuntimeEntityRecord canonical = Accept(lifetime, spawn);
Assert.True(lifetime.ApplyAcceptedSpawn(
canonical,
canonical.CreateIntegrationVersion,
spawn,
replaceGeneration: true));
Assert.False(lifetime.TryAcceptDelete(
new DeleteObject.Parsed(spawn.Guid, 7),
isLocalPlayer: false,
out _));
Assert.NotNull(lifetime.Objects.Get(spawn.Guid));
Assert.True(lifetime.TryAcceptDelete(
new DeleteObject.Parsed(spawn.Guid, 6),
isLocalPlayer: false,
out RuntimeEntityDeleteAcceptance accepted));
lifetime.CompleteAcceptedDelete(accepted);
Assert.Null(lifetime.Objects.Get(spawn.Guid));
}
[Fact]
public void DeleteAcceptance_CannotCrossOwnersOrReplay()
{
var owner = new RuntimeEntityObjectLifetime();
var other = new RuntimeEntityObjectLifetime();
WorldSession.EntitySpawn spawn = Spawn(0x70000006u, 8, "token");
RuntimeEntityRecord canonical = Accept(owner, spawn);
Assert.True(owner.ApplyAcceptedSpawn(
canonical,
canonical.CreateIntegrationVersion,
spawn,
replaceGeneration: true));
Assert.True(owner.TryAcceptDelete(
new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence),
isLocalPlayer: false,
out RuntimeEntityDeleteAcceptance acceptance));
Assert.Throws<InvalidOperationException>(
() => other.CompleteAcceptedDelete(acceptance));
owner.CompleteAcceptedDelete(acceptance);
Assert.Throws<InvalidOperationException>(
() => owner.CompleteAcceptedDelete(acceptance));
Assert.Null(owner.Objects.Get(spawn.Guid));
}
[Fact]
public void ClearObjects_DoesNotResetCanonicalEntityIdentity()
{
var lifetime = new RuntimeEntityObjectLifetime();
WorldSession.EntitySpawn spawn = Spawn(0x70000005u, 2, "clear");
RuntimeEntityRecord canonical = Accept(lifetime, spawn);
Assert.True(lifetime.ApplyAcceptedSpawn(
canonical,
canonical.CreateIntegrationVersion,
spawn,
replaceGeneration: false));
lifetime.ClearObjects();
Assert.Null(lifetime.Objects.Get(spawn.Guid));
Assert.True(lifetime.Entities.TryGetActive(spawn.Guid, out var retained));
Assert.Same(canonical, retained);
}
private static RuntimeEntityRecord Accept(
RuntimeEntityObjectLifetime lifetime,
WorldSession.EntitySpawn spawn)
{
InboundCreateResult accepted = lifetime.Entities.AcceptCreate(spawn);
return lifetime.Entities.AddActive(accepted.Snapshot);
}
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort instance,
string name)
{
var position = new CreateObject.ServerPosition(
0x0101FFFFu,
10f,
20f,
5f,
1f,
0f,
0f,
0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: instance);
var physics = new PhysicsSpawnData(
RawState: 0x408u,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
name,
null,
null,
0x09000001u,
PhysicsState: 0x408u,
InstanceSequence: instance,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
}