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>
251 lines
8.8 KiB
C#
251 lines
8.8 KiB
C#
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);
|
|
}
|
|
}
|