feat(headless): observe canonical placement receipts
This commit is contained in:
parent
74c9b155bd
commit
378ca95a67
5 changed files with 445 additions and 23 deletions
|
|
@ -0,0 +1,76 @@
|
|||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Physics;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.Headless.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Validation-only no-window observer for canonical Runtime SetPosition
|
||||
/// receipts. A headless host has no graphical sidecar to move or hide, so a
|
||||
/// valid receipt is acknowledged without re-running placement or mutating
|
||||
/// Runtime's body, controller, shadows, clocks, or worksets.
|
||||
/// </summary>
|
||||
internal sealed class HeadlessRuntimePlacementProjectionSink
|
||||
: IRuntimePlacementProjectionSink
|
||||
{
|
||||
private readonly GameRuntime _runtime;
|
||||
|
||||
internal HeadlessRuntimePlacementProjectionSink(GameRuntime runtime)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
}
|
||||
|
||||
public bool TryApply(
|
||||
in RuntimePlacementProjectionSnapshot projection)
|
||||
{
|
||||
if (projection.Kind is RuntimePlacementProjectionKind.Discard)
|
||||
{
|
||||
// Discard cancels only an unacknowledged observation. It is valid
|
||||
// even after its entity/session authority has been superseded.
|
||||
return true;
|
||||
}
|
||||
|
||||
RuntimePlacementProjectionToken token = projection.Token;
|
||||
RuntimeEntityDirectory directory = _runtime.EntityObjects.Entities;
|
||||
if (!token.IsValid
|
||||
|| token.SessionLifetimeVersion
|
||||
!= directory.SessionLifetimeVersion
|
||||
|| !directory.TryGetByLocalId(
|
||||
token.Entity.LocalEntityId,
|
||||
out RuntimeEntityRecord record)
|
||||
|| !directory.IsCurrent(record)
|
||||
|| record.Key != token.Entity
|
||||
|| !HasValidPortalShape(token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (projection.Kind is RuntimePlacementProjectionKind.Withdraw)
|
||||
return true;
|
||||
if (projection.Kind is not RuntimePlacementProjectionKind.Place)
|
||||
return false;
|
||||
|
||||
return record.PositionAuthorityVersion
|
||||
== token.PositionAuthorityVersion
|
||||
&& record.SpatialAuthorityVersion
|
||||
== token.SpatialAuthorityVersion
|
||||
&& record.PlacementCommitVersion
|
||||
== token.PlacementCommitVersion
|
||||
&& record.FullCellId == token.ExactCellId
|
||||
&& _runtime.TransitOwner.IsCurrentPlacementAuthority(
|
||||
token.Portal,
|
||||
token.ExactCellId);
|
||||
}
|
||||
|
||||
private static bool HasValidPortalShape(
|
||||
in RuntimePlacementProjectionToken token)
|
||||
{
|
||||
RuntimePortalPlacementAuthority portal = token.Portal;
|
||||
if (!portal.Present)
|
||||
return portal.IsEmpty;
|
||||
|
||||
return portal.IsValid
|
||||
&& portal.Projection.DestinationCell == token.ExactCellId;
|
||||
}
|
||||
}
|
||||
67
src/AcDream.Headless/Hosting/HeadlessSessionEventRoute.cs
Normal file
67
src/AcDream.Headless/Hosting/HeadlessSessionEventRoute.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Physics;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.Headless.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the inbound network route and the canonical placement observer for
|
||||
/// one exact headless session lifetime. Placement detaches first so no
|
||||
/// receipt can reach a retiring no-window projection while the network route
|
||||
/// is being removed or Runtime is being reset.
|
||||
/// </summary>
|
||||
internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
|
||||
{
|
||||
private readonly ILiveSessionEventRouting _events;
|
||||
private readonly GameRuntime _runtime;
|
||||
private readonly IRuntimePlacementProjectionSink _placements;
|
||||
private RuntimePlacementProjectionSubscription? _subscription;
|
||||
private bool _attachStarted;
|
||||
private bool _eventsDisposed;
|
||||
private bool _disposed;
|
||||
|
||||
internal HeadlessSessionEventRoute(
|
||||
ILiveSessionEventRouting events,
|
||||
GameRuntime runtime,
|
||||
IRuntimePlacementProjectionSink placements)
|
||||
{
|
||||
_events = events ?? throw new ArgumentNullException(nameof(events));
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_placements = placements
|
||||
?? throw new ArgumentNullException(nameof(placements));
|
||||
}
|
||||
|
||||
public void Attach()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_attachStarted)
|
||||
return;
|
||||
|
||||
// Mark the attempt before the fallible call. If Attach partially
|
||||
// succeeds and throws, LiveSessionHost's retryable rollback still
|
||||
// invokes Dispose on the underlying route.
|
||||
_attachStarted = true;
|
||||
_events.Attach();
|
||||
_subscription = new RuntimePlacementProjectionSubscription(
|
||||
_runtime,
|
||||
_placements);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
// Subscription disposal is idempotent and deliberately precedes the
|
||||
// network route. A still-pending FIFO head remains Runtime-owned for
|
||||
// the replacement route to drain.
|
||||
Interlocked.Exchange(ref _subscription, null)?.Dispose();
|
||||
if (!_eventsDisposed)
|
||||
{
|
||||
_events.Dispose();
|
||||
_eventsDisposed = true;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ using AcDream.Headless.Diagnostics;
|
|||
using AcDream.Headless.Policies;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Physics;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.Headless.Hosting;
|
||||
|
|
@ -521,7 +522,7 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private LiveSessionEventRouter CreateEventRoute(
|
||||
private ILiveSessionEventRouting CreateEventRoute(
|
||||
AcDream.Core.Net.WorldSession session)
|
||||
{
|
||||
IRuntimeDirectWorldProjection? worldProjection =
|
||||
|
|
@ -536,7 +537,7 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
message,
|
||||
Runtime.Generation.Value),
|
||||
worldProjection);
|
||||
return new LiveSessionEventRouter(
|
||||
var route = new LiveSessionEventRouter(
|
||||
session,
|
||||
entities.CreateSink(),
|
||||
new LiveEnvironmentSessionSink(
|
||||
|
|
@ -579,6 +580,10 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
Runtime.CommunicationOwner.TurbineChat,
|
||||
Runtime.CommunicationOwner.Friends,
|
||||
Runtime.CommunicationOwner.Squelch));
|
||||
return new HeadlessSessionEventRoute(
|
||||
route,
|
||||
Runtime,
|
||||
new HeadlessRuntimePlacementProjectionSink(Runtime));
|
||||
}
|
||||
|
||||
private static LiveSessionCharacterSelector MapCharacterSelector(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue