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:
parent
d9bf4c4960
commit
5ef8b5371d
40 changed files with 712 additions and 170 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue