feat(runtime): publish dormant local physics ownership
This commit is contained in:
parent
442cb8f97b
commit
22651c823d
10 changed files with 1617 additions and 25 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue