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
|
|
@ -80,6 +80,64 @@ public interface ILiveEntityResourceLifecycle
|
|||
void Unregister(WorldEntity entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exact-incarnation App component cleanup invoked by the canonical runtime
|
||||
/// after it has retired the active identity. Mesh/script resource lifetime is
|
||||
/// owned separately by <see cref="ILiveEntityResourceLifecycle"/>.
|
||||
/// </summary>
|
||||
internal interface ILiveEntityRuntimeComponentLifecycle
|
||||
{
|
||||
void TearDown(LiveEntityRecord record);
|
||||
}
|
||||
|
||||
internal sealed class NullLiveEntityRuntimeComponentLifecycle :
|
||||
ILiveEntityRuntimeComponentLifecycle
|
||||
{
|
||||
public static NullLiveEntityRuntimeComponentLifecycle Instance { get; } = new();
|
||||
private NullLiveEntityRuntimeComponentLifecycle() { }
|
||||
public void TearDown(LiveEntityRecord record) { }
|
||||
}
|
||||
|
||||
internal sealed class DelegateLiveEntityRuntimeComponentLifecycle(
|
||||
Action<LiveEntityRecord> tearDown) : ILiveEntityRuntimeComponentLifecycle
|
||||
{
|
||||
private readonly Action<LiveEntityRecord> _tearDown =
|
||||
tearDown ?? throw new ArgumentNullException(nameof(tearDown));
|
||||
|
||||
public void TearDown(LiveEntityRecord record) => _tearDown(record);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composition bridge for the runtime-to-hydration construction cycle. It is
|
||||
/// deliberately fail-fast and single-assignment: a live session must not start
|
||||
/// before the exact component owner is bound, and ownership cannot be replaced
|
||||
/// during a session.
|
||||
/// </summary>
|
||||
internal sealed class DeferredLiveEntityRuntimeComponentLifecycle :
|
||||
ILiveEntityRuntimeComponentLifecycle
|
||||
{
|
||||
private ILiveEntityRuntimeComponentLifecycle? _inner;
|
||||
|
||||
public bool IsBound => Volatile.Read(ref _inner) is not null;
|
||||
|
||||
public void Bind(ILiveEntityRuntimeComponentLifecycle lifecycle)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(lifecycle);
|
||||
if (ReferenceEquals(lifecycle, this))
|
||||
throw new ArgumentException("A lifecycle bridge cannot bind to itself.", nameof(lifecycle));
|
||||
if (Interlocked.CompareExchange(ref _inner, lifecycle, null) is not null)
|
||||
throw new InvalidOperationException("The live-entity runtime component lifecycle is already bound.");
|
||||
}
|
||||
|
||||
public void TearDown(LiveEntityRecord record)
|
||||
{
|
||||
ILiveEntityRuntimeComponentLifecycle lifecycle = Volatile.Read(ref _inner)
|
||||
?? throw new InvalidOperationException(
|
||||
"The live-entity runtime component lifecycle has not been bound.");
|
||||
lifecycle.TearDown(record);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Delegate adapter used by the App composition root.</summary>
|
||||
public sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceLifecycle
|
||||
{
|
||||
|
|
@ -300,7 +358,7 @@ public sealed class LiveEntityRuntime
|
|||
|
||||
private readonly GpuWorldState _spatial;
|
||||
private readonly ILiveEntityResourceLifecycle _resources;
|
||||
private readonly Action<LiveEntityRecord>? _tearDownRuntimeComponents;
|
||||
private readonly ILiveEntityRuntimeComponentLifecycle _runtimeComponentLifecycle;
|
||||
private readonly InboundPhysicsStateController _inbound = new();
|
||||
private readonly Dictionary<uint, LiveEntityRecord> _recordsByGuid = new();
|
||||
// Records leave the active gameplay map before arbitrary teardown callbacks
|
||||
|
|
@ -338,12 +396,38 @@ public sealed class LiveEntityRuntime
|
|||
public LiveEntityRuntime(
|
||||
GpuWorldState spatial,
|
||||
ILiveEntityResourceLifecycle resources,
|
||||
Action<LiveEntityRecord>? tearDownRuntimeComponents = null,
|
||||
uint firstLocalEntityId = FirstLiveEntityId)
|
||||
: this(
|
||||
spatial,
|
||||
resources,
|
||||
NullLiveEntityRuntimeComponentLifecycle.Instance,
|
||||
firstLocalEntityId)
|
||||
{
|
||||
}
|
||||
|
||||
public LiveEntityRuntime(
|
||||
GpuWorldState spatial,
|
||||
ILiveEntityResourceLifecycle resources,
|
||||
Action<LiveEntityRecord> tearDownRuntimeComponents,
|
||||
uint firstLocalEntityId = FirstLiveEntityId)
|
||||
: this(
|
||||
spatial,
|
||||
resources,
|
||||
new DelegateLiveEntityRuntimeComponentLifecycle(tearDownRuntimeComponents),
|
||||
firstLocalEntityId)
|
||||
{
|
||||
}
|
||||
|
||||
internal LiveEntityRuntime(
|
||||
GpuWorldState spatial,
|
||||
ILiveEntityResourceLifecycle resources,
|
||||
ILiveEntityRuntimeComponentLifecycle? runtimeComponentLifecycle,
|
||||
uint firstLocalEntityId = FirstLiveEntityId)
|
||||
{
|
||||
_spatial = spatial ?? throw new ArgumentNullException(nameof(spatial));
|
||||
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
||||
_tearDownRuntimeComponents = tearDownRuntimeComponents;
|
||||
_runtimeComponentLifecycle = runtimeComponentLifecycle
|
||||
?? throw new ArgumentNullException(nameof(runtimeComponentLifecycle));
|
||||
if (firstLocalEntityId is < FirstLiveEntityId or > LastLiveEntityId)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(firstLocalEntityId),
|
||||
|
|
@ -2239,8 +2323,7 @@ public sealed class LiveEntityRuntime
|
|||
if (!record.RuntimeComponentsTeardownCompleted)
|
||||
{
|
||||
record.RuntimeComponentsTeardownCompleted =
|
||||
_tearDownRuntimeComponents is null
|
||||
|| TryCleanup(() => _tearDownRuntimeComponents(record));
|
||||
TryCleanup(() => _runtimeComponentLifecycle.TearDown(record));
|
||||
}
|
||||
|
||||
if (record.WorldEntity is { } entity)
|
||||
|
|
|
|||
49
src/AcDream.App/World/LiveWorldOriginState.cs
Normal file
49
src/AcDream.App/World/LiveWorldOriginState.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Session-scoped owner of the landblock origin used to translate retail
|
||||
/// full-cell coordinates into the current streamed world coordinate system.
|
||||
/// A placeholder can exist before the first authoritative player position;
|
||||
/// <see cref="IsKnown"/> distinguishes it from a server-confirmed origin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This owner is update/render-thread confined with the rest of live world
|
||||
/// mutation. It intentionally exposes no cross-thread snapshot contract.
|
||||
/// </remarks>
|
||||
internal sealed class LiveWorldOriginState
|
||||
{
|
||||
public int CenterX { get; private set; }
|
||||
public int CenterY { get; private set; }
|
||||
public bool IsKnown { get; private set; }
|
||||
|
||||
public void SetPlaceholder(int centerX, int centerY)
|
||||
{
|
||||
CenterX = centerX;
|
||||
CenterY = centerY;
|
||||
IsKnown = false;
|
||||
}
|
||||
|
||||
public bool TryInitialize(int centerX, int centerY)
|
||||
{
|
||||
if (IsKnown)
|
||||
return false;
|
||||
|
||||
CenterX = centerX;
|
||||
CenterY = centerY;
|
||||
IsKnown = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Recenter(int centerX, int centerY)
|
||||
{
|
||||
CenterX = centerX;
|
||||
CenterY = centerY;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends the accepted-origin lifetime while retaining the last coordinates
|
||||
/// as the next placeholder. Consumers must gate them on
|
||||
/// <see cref="IsKnown"/> until the next accepted player position.
|
||||
/// </summary>
|
||||
public void Reset() => IsKnown = false;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue