feat(runtime): publish dormant local physics ownership

This commit is contained in:
Erik 2026-08-01 10:01:30 +02:00
parent 442cb8f97b
commit 22651c823d
10 changed files with 1617 additions and 25 deletions

View file

@ -0,0 +1,316 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Gameplay;
internal enum RuntimeLocalPlayerPhysicsPublicationStatus
{
Prepared,
Committed,
RejectedAuthority,
RejectedToken,
Discarded,
}
internal readonly record struct RuntimeLocalPlayerPhysicsPublicationToken(
RuntimeEntityKey Entity,
RuntimeEntityPlacementToken Placement,
ulong PublicationId,
uint LocalPlayerServerGuid,
long LocalPlayerIdentityRevision,
ulong PhysicsOwnershipEpoch,
ulong ObjectClockEpoch,
ulong ControllerOwnershipEpoch,
ulong SessionGenerationAuthority)
{
internal bool IsValid => PublicationId != 0UL
&& LocalPlayerServerGuid != 0u
&& Placement.IsValid
&& Entity == Placement.Entity;
}
internal readonly record struct RuntimeLocalPlayerPhysicsCandidateSnapshot(
Vector3 Position,
Quaternion Orientation,
uint CellId,
Vector3 CellLocalPosition,
PhysicsStateFlags State,
TransientStateFlags TransientState,
bool InWorld);
public readonly record struct
RuntimeLocalPlayerPhysicsPublicationOwnershipSnapshot(
bool IsBound,
bool IsDisposed,
int CandidateCount,
ulong LastPublicationId)
{
internal bool IsConverged => !IsBound
|| (IsDisposed && CandidateCount == 0);
}
/// <summary>
/// Dormant, presentation-independent owner of local-player body/controller
/// candidates. Preparation owns a private body and clock. Commit is one
/// callback-free update-thread transaction which assigns that exact body to
/// the canonical entity and a dormant movement controller. It deliberately
/// does not activate the controller or
/// consume SetPosition or publish world residence, host, shadow, ordinary
/// workset, FullCell, or presentation state; those belong to the subsequent
/// activation transaction.
/// </summary>
internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
{
private sealed class Candidate
{
internal required RuntimeLocalPlayerPhysicsPublicationToken Token
{ get; init; }
internal required RuntimeEntityRecord Record { get; init; }
internal required RuntimeSetPositionCommand PlacementCommand
{ get; init; }
internal required PlayerMovementController Controller { get; init; }
internal required PhysicsBody Body { get; init; }
}
private readonly RuntimeEntityDirectory _entities;
private readonly RuntimePhysicsState _physics;
private readonly RuntimeLocalPlayerMovementState _movement;
private readonly RuntimeLocalPlayerIdentityState _identity;
private Candidate? _candidate;
private ulong _nextPublicationId;
private bool _disposed;
internal RuntimeLocalPlayerPhysicsPublicationState(
RuntimeEntityDirectory entities,
RuntimePhysicsState physics,
RuntimeLocalPlayerMovementState movement,
RuntimeLocalPlayerIdentityState identity)
{
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_movement = movement ?? throw new ArgumentNullException(nameof(movement));
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
}
internal RuntimeLocalPlayerPhysicsPublicationStatus Prepare(
RuntimeEntityRecord record,
in RuntimeEntityPlacementToken placement,
in RuntimeSetPositionCommand command,
PlayerMovementConstructionOptions options,
out RuntimeLocalPlayerPhysicsPublicationToken token)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentNullException.ThrowIfNull(record);
token = default;
if (!CanPrepare(record, placement, command))
return RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority;
var controller = PlayerMovementController.CreatePublicationCandidate(
_physics.Engine,
options);
controller.LocalEntityId = record.Key!.Value.LocalEntityId;
controller.StepUpHeight = command.Physics.StepUpHeight;
controller.StepDownHeight = command.Physics.StepDownHeight;
controller.SphereList = command.Physics.Spheres;
controller.ObjectScale = command.Physics.Scale;
controller.PreparePositionForCommit(
command.Physics.Position,
command.Physics.CellId,
command.Physics.CellLocalPosition);
controller.SetBodyOrientation(command.Physics.Orientation);
controller.ApplyPhysicsState(record.FinalPhysicsState);
PhysicsBody body = controller.PhysicsBody;
// This checkpoint publishes ownership only. The subsequent canonical
// SetPosition transaction is the sole authority which may enter the
// body into world simulation and activate its ordinary workset.
body.InWorld = false;
body.TransientState &= ~TransientStateFlags.Active;
controller.SealPublicationCandidate();
// Candidate construction is intentionally private, but every accepted
// authority is rechecked after it so future content/configuration work
// cannot accidentally create a callback-shaped stale publication.
if (!CanPrepare(record, placement, command))
{
controller.DiscardRuntimeCandidate();
return RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority;
}
DiscardCurrent();
token = new RuntimeLocalPlayerPhysicsPublicationToken(
record.Key.Value,
placement,
checked(++_nextPublicationId),
_identity.ServerGuid,
_identity.Revision,
record.PhysicsOwnershipEpoch,
record.ObjectClockEpoch,
_movement.ControllerOwnershipEpoch,
_entities.SessionLifetimeVersion);
_candidate = new Candidate
{
Token = token,
Record = record,
PlacementCommand = command,
Controller = controller,
Body = body,
};
return RuntimeLocalPlayerPhysicsPublicationStatus.Prepared;
}
internal RuntimeLocalPlayerPhysicsPublicationStatus Commit(
in RuntimeLocalPlayerPhysicsPublicationToken token)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!token.IsValid
|| _candidate is not { } candidate
|| candidate.Token != token)
{
return RuntimeLocalPlayerPhysicsPublicationStatus.RejectedToken;
}
if (!IsCurrent(candidate))
{
DiscardCurrent();
return RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority;
}
// All validation is complete. The remaining stores are callback-free,
// non-allocating, and cannot fail on this single Runtime update thread.
// The controller remains RuntimeOwnedDormant; the subsequent world
// activation transaction is the only authority allowed to make it live.
candidate.Controller.CommitRuntimeOwnership(
candidate.Record.ObjectClock);
candidate.Record.SetPhysicsBody(candidate.Body);
_movement.CommitRuntimeOwnedController(candidate.Controller);
_candidate = null;
return RuntimeLocalPlayerPhysicsPublicationStatus.Committed;
}
internal RuntimeLocalPlayerPhysicsPublicationStatus Discard(
in RuntimeLocalPlayerPhysicsPublicationToken token)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!token.IsValid
|| _candidate is not { } candidate
|| candidate.Token != token)
{
return RuntimeLocalPlayerPhysicsPublicationStatus.RejectedToken;
}
DiscardCurrent();
return RuntimeLocalPlayerPhysicsPublicationStatus.Discarded;
}
internal bool TryCaptureCandidateSnapshot(
in RuntimeLocalPlayerPhysicsPublicationToken token,
out RuntimeLocalPlayerPhysicsCandidateSnapshot snapshot)
{
if (!_disposed
&& token.IsValid
&& _candidate is { } candidate
&& candidate.Token == token)
{
PhysicsBody body = candidate.Body;
snapshot = new RuntimeLocalPlayerPhysicsCandidateSnapshot(
body.Position,
body.Orientation,
body.CellPosition.ObjCellId,
body.CellPosition.Frame.Origin,
body.State,
body.TransientState,
body.InWorld);
return true;
}
snapshot = default;
return false;
}
internal RuntimeLocalPlayerPhysicsPublicationOwnershipSnapshot
CaptureOwnership() => new(
IsBound: true,
_disposed,
_candidate is null ? 0 : 1,
_nextPublicationId);
internal void ResetSession()
{
ObjectDisposedException.ThrowIf(_disposed, this);
DiscardCurrent();
}
public void Dispose()
{
if (_disposed)
return;
DiscardCurrent();
_disposed = true;
}
private bool CanPrepare(
RuntimeEntityRecord record,
in RuntimeEntityPlacementToken placement,
in RuntimeSetPositionCommand command) =>
record.Key is { } key
&& key == placement.Entity
&& _entities.IsCurrent(record)
&& !record.DeleteAcceptedForTeardown
&& command.Kind is RuntimeSetPositionOperationKind.InitialLogin
or RuntimeSetPositionOperationKind.LocalAuthoritative
&& command.Physics.MovingEntityId == key.LocalEntityId
&& !_identity.IsDisposed
&& _identity.ServerGuid != 0u
&& _identity.ServerGuid == record.ServerGuid
&& record.PhysicsBody is null
&& _movement.Controller is null
&& record.PhysicsHost is null
&& record.RemoteMotion is null
&& record.Projectile is null
&& !record.PhysicsBodyAcquisitionInProgress
&& !record.RemoteMotionBindingInProgress
&& !record.ProjectileBindingInProgress
&& !record.RequiresRemotePlacementRuntime
&& _physics.SetPosition.IsExactPreparedPlacementCurrent(
record,
placement,
command);
private bool IsCurrent(Candidate candidate) =>
candidate.Controller.IsSealedPublicationCandidate
&& candidate.Controller.OwnsPhysicsBody(candidate.Body)
&& _entities.SessionLifetimeVersion
== candidate.Token.SessionGenerationAuthority
&& _entities.IsCurrent(candidate.Record)
&& candidate.Record.Key == candidate.Token.Entity
&& !_identity.IsDisposed
&& _identity.ServerGuid == candidate.Token.LocalPlayerServerGuid
&& _identity.ServerGuid == candidate.Record.ServerGuid
&& _identity.Revision == candidate.Token.LocalPlayerIdentityRevision
&& candidate.Record.PhysicsOwnershipEpoch
== candidate.Token.PhysicsOwnershipEpoch
&& candidate.Record.ObjectClockEpoch
== candidate.Token.ObjectClockEpoch
&& _movement.CanCommitRuntimeOwnedController(
candidate.Token.ControllerOwnershipEpoch,
expectedController: null)
&& candidate.Record.PhysicsBody is null
&& candidate.Record.PhysicsHost is null
&& candidate.Record.RemoteMotion is null
&& candidate.Record.Projectile is null
&& !candidate.Record.PhysicsBodyAcquisitionInProgress
&& !candidate.Record.RemoteMotionBindingInProgress
&& !candidate.Record.ProjectileBindingInProgress
&& !candidate.Record.RequiresRemotePlacementRuntime
&& !candidate.Record.DeleteAcceptedForTeardown
&& _physics.SetPosition.IsExactPreparedPlacementCurrent(
candidate.Record,
candidate.Token.Placement,
candidate.PlacementCommand);
private void DiscardCurrent()
{
Candidate? candidate = _candidate;
_candidate = null;
candidate?.Controller.DiscardRuntimeCandidate();
}
}