refactor(world): establish live entity composition seams
This commit is contained in:
parent
db5a11707d
commit
d68c83d1a5
5 changed files with 357 additions and 21 deletions
|
|
@ -0,0 +1,137 @@
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
public sealed class DeferredLiveEntityRuntimeComponentLifecycleTests
|
||||
{
|
||||
[Fact]
|
||||
public void TearDownBeforeBind_FailsFast()
|
||||
{
|
||||
var bridge = new DeferredLiveEntityRuntimeComponentLifecycle();
|
||||
|
||||
Assert.False(bridge.IsBound);
|
||||
Assert.Throws<InvalidOperationException>(() => bridge.TearDown(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_IsSingleAssignmentAndForwardsExactRecord()
|
||||
{
|
||||
var bridge = new DeferredLiveEntityRuntimeComponentLifecycle();
|
||||
LiveEntityRecord? observed = null;
|
||||
var owner = new DelegateLiveEntityRuntimeComponentLifecycle(record => observed = record);
|
||||
var replacement = new DelegateLiveEntityRuntimeComponentLifecycle(_ => { });
|
||||
LiveEntityRecord record = CreateRecord();
|
||||
|
||||
bridge.Bind(owner);
|
||||
bridge.TearDown(record);
|
||||
|
||||
Assert.True(bridge.IsBound);
|
||||
Assert.Same(record, observed);
|
||||
Assert.Throws<InvalidOperationException>(() => bridge.Bind(replacement));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BindRejectsNullAndSelf()
|
||||
{
|
||||
var bridge = new DeferredLiveEntityRuntimeComponentLifecycle();
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => bridge.Bind(null!));
|
||||
Assert.Throws<ArgumentException>(() => bridge.Bind(bridge));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConcurrentBind_HasOneWinnerAndPublishesThatOwner()
|
||||
{
|
||||
var bridge = new DeferredLiveEntityRuntimeComponentLifecycle();
|
||||
int firstCalls = 0;
|
||||
int secondCalls = 0;
|
||||
var owners = new ILiveEntityRuntimeComponentLifecycle[]
|
||||
{
|
||||
new DelegateLiveEntityRuntimeComponentLifecycle(_ => Interlocked.Increment(ref firstCalls)),
|
||||
new DelegateLiveEntityRuntimeComponentLifecycle(_ => Interlocked.Increment(ref secondCalls)),
|
||||
};
|
||||
using var ready = new CountdownEvent(2);
|
||||
using var start = new ManualResetEventSlim();
|
||||
|
||||
Task<bool>[] attempts = owners.Select(owner => Task.Run(() =>
|
||||
{
|
||||
ready.Signal();
|
||||
start.Wait();
|
||||
try
|
||||
{
|
||||
bridge.Bind(owner);
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
})).ToArray();
|
||||
|
||||
ready.Wait();
|
||||
start.Set();
|
||||
bool[] results = await Task.WhenAll(attempts);
|
||||
LiveEntityRecord record = CreateRecord();
|
||||
bridge.TearDown(record);
|
||||
|
||||
Assert.Single(results, value => value);
|
||||
Assert.True(bridge.IsBound);
|
||||
Assert.Equal(1, firstCalls + secondCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeTeardownBeforeBind_RetainsTombstoneAndRetryConverges()
|
||||
{
|
||||
const uint guid = 0x70000002u;
|
||||
var bridge = new DeferredLiveEntityRuntimeComponentLifecycle();
|
||||
var runtime = new LiveEntityRuntime(
|
||||
new GpuWorldState(),
|
||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
|
||||
bridge);
|
||||
WorldSession.EntitySpawn spawn = CreateSpawn(guid);
|
||||
LiveEntityRecord record = runtime.RegisterLiveEntity(spawn).Record!;
|
||||
var delete = new DeleteObject.Parsed(guid, InstanceSequence: 1);
|
||||
|
||||
Assert.Throws<AggregateException>(() =>
|
||||
runtime.UnregisterLiveEntity(delete, isLocalPlayer: false));
|
||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||
|
||||
int calls = 0;
|
||||
bridge.Bind(new DelegateLiveEntityRuntimeComponentLifecycle(exact =>
|
||||
{
|
||||
Assert.Same(record, exact);
|
||||
calls++;
|
||||
}));
|
||||
|
||||
Assert.True(runtime.UnregisterLiveEntity(delete, isLocalPlayer: false));
|
||||
Assert.Equal(1, calls);
|
||||
Assert.Equal(0, runtime.PendingTeardownCount);
|
||||
}
|
||||
|
||||
private static LiveEntityRecord CreateRecord()
|
||||
{
|
||||
var runtime = new LiveEntityRuntime(
|
||||
new GpuWorldState(),
|
||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||
return runtime.RegisterLiveEntity(CreateSpawn(0x70000001u)).Record!;
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn CreateSpawn(uint guid) =>
|
||||
new(
|
||||
Guid: guid,
|
||||
Position: null,
|
||||
SetupTableId: null,
|
||||
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
|
||||
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
|
||||
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||
BasePaletteId: null,
|
||||
ObjScale: null,
|
||||
Name: "test",
|
||||
ItemType: null,
|
||||
MotionState: null,
|
||||
MotionTableId: null,
|
||||
InstanceSequence: 1);
|
||||
}
|
||||
62
tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs
Normal file
62
tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using AcDream.App.World;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
public sealed class LiveWorldOriginStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void Placeholder_IsNotAuthoritativeUntilFirstInitialization()
|
||||
{
|
||||
var state = new LiveWorldOriginState();
|
||||
|
||||
state.SetPlaceholder(42, 43);
|
||||
|
||||
Assert.Equal(42, state.CenterX);
|
||||
Assert.Equal(43, state.CenterY);
|
||||
Assert.False(state.IsKnown);
|
||||
Assert.True(state.TryInitialize(10, 11));
|
||||
Assert.Equal(10, state.CenterX);
|
||||
Assert.Equal(11, state.CenterY);
|
||||
Assert.True(state.IsKnown);
|
||||
Assert.False(state.TryInitialize(20, 21));
|
||||
Assert.Equal(10, state.CenterX);
|
||||
Assert.Equal(11, state.CenterY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Recenter_ReplacesKnownOriginAndResetRetainsItAsPlaceholder()
|
||||
{
|
||||
var state = new LiveWorldOriginState();
|
||||
Assert.True(state.TryInitialize(1, 2));
|
||||
|
||||
state.Recenter(3, 4);
|
||||
state.Reset();
|
||||
|
||||
Assert.Equal(3, state.CenterX);
|
||||
Assert.Equal(4, state.CenterY);
|
||||
Assert.False(state.IsKnown);
|
||||
Assert.True(state.TryInitialize(5, 6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecenterBeforeInitialization_PreservesUnknownAndResetIsIdempotent()
|
||||
{
|
||||
var state = new LiveWorldOriginState();
|
||||
state.SetPlaceholder(20, 21);
|
||||
|
||||
state.Recenter(30, 31);
|
||||
state.Reset();
|
||||
state.Reset();
|
||||
|
||||
Assert.Equal(30, state.CenterX);
|
||||
Assert.Equal(31, state.CenterY);
|
||||
Assert.False(state.IsKnown);
|
||||
|
||||
Assert.True(state.TryInitialize(40, 41));
|
||||
state.Recenter(50, 51);
|
||||
|
||||
Assert.True(state.IsKnown);
|
||||
Assert.Equal(50, state.CenterX);
|
||||
Assert.Equal(51, state.CenterY);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue