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
|
|
@ -342,7 +342,7 @@ public sealed class RuntimeEntityDirectory
|
|||
AcDream.Core.Physics.PhysicsBody? body)
|
||||
{
|
||||
EnsureKnown(record);
|
||||
record.PhysicsBody = body;
|
||||
record.SetPhysicsBody(body);
|
||||
}
|
||||
|
||||
public void SetPhysicsBodyAcquisitionInProgress(
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ public sealed class RuntimeEntityRecord
|
|||
public uint CanonicalLandblockId { get; internal set; }
|
||||
public uint RawPhysicsState { get; internal set; }
|
||||
public PhysicsStateFlags FinalPhysicsState { get; internal set; }
|
||||
public ulong PhysicsOwnershipEpoch { get; private set; }
|
||||
public ulong SpatialAuthorityVersion { get; private set; }
|
||||
public ulong PlacementCommitVersion { get; private set; }
|
||||
public ulong PhysicsStateMutationVersion { get; private set; }
|
||||
|
|
@ -68,7 +69,7 @@ public sealed class RuntimeEntityRecord
|
|||
/// object reference.
|
||||
/// </summary>
|
||||
public bool HasPartArray { get; internal set; }
|
||||
public PhysicsBody? PhysicsBody { get; internal set; }
|
||||
public PhysicsBody? PhysicsBody { get; private set; }
|
||||
public bool PhysicsBodyAcquisitionInProgress { get; internal set; }
|
||||
public IRuntimeRemoteMotion? RemoteMotion { get; internal set; }
|
||||
public bool RemoteMotionBindingInProgress { get; internal set; }
|
||||
|
|
@ -172,6 +173,14 @@ public sealed class RuntimeEntityRecord
|
|||
FinalPhysicsState = state;
|
||||
}
|
||||
|
||||
internal void SetPhysicsBody(PhysicsBody? body)
|
||||
{
|
||||
if (ReferenceEquals(PhysicsBody, body))
|
||||
return;
|
||||
PhysicsBody = body;
|
||||
PhysicsOwnershipEpoch++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail collision reporting clears Missile, AlignPath, and PathClipped
|
||||
/// directly on the live CPhysicsObj. Keep the canonical record and its
|
||||
|
|
|
|||
|
|
@ -256,6 +256,13 @@ public sealed class GameRuntime
|
|||
context.Character,
|
||||
context.PlayerIdentity);
|
||||
|
||||
context.Movement.AttachPhysicsPublication(
|
||||
new RuntimeLocalPlayerPhysicsPublicationState(
|
||||
context.EntityObjects.Entities,
|
||||
context.EntityObjects.Physics,
|
||||
context.Movement,
|
||||
context.PlayerIdentity));
|
||||
|
||||
context.EntityObjects.BindEventContext(
|
||||
() => generationReset.ActiveRetiringGeneration
|
||||
?? context.Session.Generation,
|
||||
|
|
@ -301,6 +308,8 @@ public sealed class GameRuntime
|
|||
public RuntimeCommunicationState CommunicationOwner { get; }
|
||||
public RuntimeActionState ActionOwner { get; }
|
||||
public RuntimeLocalPlayerMovementState MovementOwner { get; }
|
||||
internal RuntimeLocalPlayerPhysicsPublicationState
|
||||
LocalPlayerPhysicsPublication => MovementOwner.PhysicsPublication;
|
||||
public RuntimeWorldEnvironmentState EnvironmentOwner { get; }
|
||||
public RuntimeWorldTransitState TransitOwner { get; }
|
||||
public RuntimeGenerationReset GenerationReset { get; }
|
||||
|
|
|
|||
|
|
@ -121,6 +121,17 @@ public readonly record struct MovementResult(
|
|||
/// </summary>
|
||||
public enum PlayerState { InWorld, PortalSpace }
|
||||
|
||||
internal enum PlayerMovementControllerPublicationLifecycle
|
||||
{
|
||||
StandalonePublished,
|
||||
CandidatePreparing,
|
||||
CandidateSealed,
|
||||
RuntimeOwnedDormant,
|
||||
RuntimePublished,
|
||||
RuntimeRetired,
|
||||
Discarded,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame player movement controller. Reads input, drives the
|
||||
/// ported PhysicsBody + MotionInterpreter, tracks motion state for
|
||||
|
|
@ -140,6 +151,17 @@ public sealed class PlayerMovementController
|
|||
private readonly PhysicsBody _body;
|
||||
private readonly MotionInterpreter _motion;
|
||||
private readonly PlayerWeenie _weenie;
|
||||
private readonly AcDream.Core.Physics.Motion.MovementManager
|
||||
_movementManager;
|
||||
private float _stepUpHeight = 0.4f;
|
||||
private float _stepDownHeight = 0.4f;
|
||||
private ObjectInfoState _ownPvpFlags = ObjectInfoState.None;
|
||||
private System.Collections.Immutable.ImmutableArray<FlatCollisionSphere>
|
||||
_sphereList;
|
||||
private float _objectScale = 1f;
|
||||
private PlayerState _state = PlayerState.InWorld;
|
||||
private uint _localEntityId;
|
||||
private AcDream.Core.Physics.Motion.PositionManager? _positionManager;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum Z increase per movement step before the move is rejected.
|
||||
|
|
@ -151,7 +173,15 @@ public sealed class PlayerMovementController
|
|||
/// Authoritative source is the player's <c>Setup.StepUpHeight</c> set
|
||||
/// in GameWindow.cs at world-entry time.
|
||||
/// </summary>
|
||||
public float StepUpHeight { get; set; } = 0.4f;
|
||||
public float StepUpHeight
|
||||
{
|
||||
get => _stepUpHeight;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_stepUpHeight = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// L.2.3a (2026-04-29): how far below the foot the step-down probe
|
||||
|
|
@ -161,7 +191,15 @@ public sealed class PlayerMovementController
|
|||
/// the ground 25 cm below produced a one-frame contact-plane gap — the
|
||||
/// animation system briefly flickered to falling.
|
||||
/// </summary>
|
||||
public float StepDownHeight { get; set; } = 0.4f;
|
||||
public float StepDownHeight
|
||||
{
|
||||
get => _stepDownHeight;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_stepDownHeight = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TS-23 (Campaign P Slice P3, 2026-07-30): the local player's own
|
||||
|
|
@ -176,7 +214,15 @@ public sealed class PlayerMovementController
|
|||
/// hardcoded <c>IsPlayer | EdgeSlide</c> — the non-PK invariant this
|
||||
/// port must not break.
|
||||
/// </summary>
|
||||
public ObjectInfoState OwnPvpFlags { get; set; } = ObjectInfoState.None;
|
||||
public ObjectInfoState OwnPvpFlags
|
||||
{
|
||||
get => _ownPvpFlags;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_ownPvpFlags = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TS-46 (2026-07-30): the player's own Setup ≤2-sphere list (dat
|
||||
|
|
@ -191,14 +237,31 @@ public sealed class PlayerMovementController
|
|||
/// (0,0,1.350) r=.48, a 5 mm improvement over the reconstruction's
|
||||
/// (0,0,0.48) + (0,0,1.355).
|
||||
/// </summary>
|
||||
public System.Collections.Immutable.ImmutableArray<FlatCollisionSphere> SphereList { get; set; }
|
||||
public System.Collections.Immutable.ImmutableArray<FlatCollisionSphere>
|
||||
SphereList
|
||||
{
|
||||
get => _sphereList;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_sphereList = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CPhysicsObj::m_scale</c>. Grounded CSequence root
|
||||
/// displacement is multiplied by this value before PositionManager
|
||||
/// composition (<c>UpdatePositionInternal @ 0x00512C30</c>).
|
||||
/// </summary>
|
||||
public float ObjectScale { get; set; } = 1f;
|
||||
public float ObjectScale
|
||||
{
|
||||
get => _objectScale;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_objectScale = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current portal-space state. Set to PortalSpace when the server sends
|
||||
|
|
@ -207,7 +270,15 @@ public sealed class PlayerMovementController
|
|||
/// While in PortalSpace, Update returns immediately with a zero-movement
|
||||
/// result so no WASD input or physics is processed.
|
||||
/// </summary>
|
||||
public PlayerState State { get; set; } = PlayerState.InWorld;
|
||||
public PlayerState State
|
||||
{
|
||||
get => _state;
|
||||
set
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
_state = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Horizontal projection of the authoritative body quaternion. Assigning
|
||||
|
|
@ -221,6 +292,7 @@ public sealed class PlayerMovementController
|
|||
AcDream.Core.Physics.Motion.MoveToMath.GetHeading(_body.Orientation));
|
||||
set
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
float wrapped = value;
|
||||
while (wrapped > MathF.PI) wrapped -= 2f * MathF.PI;
|
||||
while (wrapped < -MathF.PI) wrapped += 2f * MathF.PI;
|
||||
|
|
@ -253,6 +325,7 @@ public sealed class PlayerMovementController
|
|||
internal bool TryGetOutboundPosition(
|
||||
out AcDream.Core.Physics.Position outboundPosition)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
AcDream.Core.Physics.Position canonical = _body.CellPosition;
|
||||
outboundPosition = new AcDream.Core.Physics.Position(
|
||||
canonical.ObjCellId,
|
||||
|
|
@ -273,7 +346,15 @@ public sealed class PlayerMovementController
|
|||
/// sweep collides with its own ShadowEntry registered at
|
||||
/// GameWindow.cs:2545 — see #42.
|
||||
/// </summary>
|
||||
public uint LocalEntityId { get; set; }
|
||||
public uint LocalEntityId
|
||||
{
|
||||
get => _localEntityId;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_localEntityId = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the canonical server PhysicsState to the local body. Retail's
|
||||
|
|
@ -282,6 +363,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public void ApplyPhysicsState(PhysicsStateFlags state)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_body.State = state;
|
||||
_body.calc_acceleration();
|
||||
}
|
||||
|
|
@ -431,7 +513,8 @@ public sealed class PlayerMovementController
|
|||
//
|
||||
// ACE: PhysicsObj.UpdateObject (Physics.cs).
|
||||
// Named-retail: CPhysicsObj::update_object (acclient_2013_pseudo_c.txt:283950).
|
||||
private readonly RetailObjectQuantumClock _objectClock;
|
||||
private RetailObjectQuantumClock _objectClock;
|
||||
private PlayerMovementControllerPublicationLifecycle _publicationLifecycle;
|
||||
private Vector3 _prevPhysicsPos;
|
||||
private Vector3 _currPhysicsPos;
|
||||
private Action<float, AcDream.Core.Physics.Motion.MotionDeltaFrame>?
|
||||
|
|
@ -463,7 +546,14 @@ public sealed class PlayerMovementController
|
|||
/// <c>DriveServerAutoWalk</c> occupied and relays <c>HitGround()</c>
|
||||
/// (0x00524300, minterp first then moveto).
|
||||
/// </summary>
|
||||
public AcDream.Core.Physics.Motion.MovementManager Movement { get; }
|
||||
public AcDream.Core.Physics.Motion.MovementManager Movement
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
return _movementManager;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4-V5: the local player's verbatim retail <c>MoveToManager</c>
|
||||
|
|
@ -486,6 +576,7 @@ public sealed class PlayerMovementController
|
|||
get => Movement.MoveTo;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
var mtm = value ?? throw new ArgumentNullException(nameof(value));
|
||||
Movement.MoveToFactory = () => mtm;
|
||||
Movement.MakeMoveToManager();
|
||||
|
|
@ -509,12 +600,28 @@ public sealed class PlayerMovementController
|
|||
/// <see cref="BlipPosition"/> arms the leash without tearing it down first
|
||||
/// (retail <c>SmartBox::BlipPlayer</c> survives motion/velocity/stick).
|
||||
/// </summary>
|
||||
public AcDream.Core.Physics.Motion.PositionManager? PositionManager { get; set; }
|
||||
public AcDream.Core.Physics.Motion.PositionManager? PositionManager
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
return _positionManager;
|
||||
}
|
||||
set
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_positionManager = value;
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerMovementController(
|
||||
PhysicsEngine physics,
|
||||
RetailObjectQuantumClock? objectClock = null)
|
||||
: this(physics, objectClock, PlayerMovementConstructionOptions.Fallback)
|
||||
: this(
|
||||
physics,
|
||||
objectClock,
|
||||
PlayerMovementConstructionOptions.Fallback,
|
||||
PlayerMovementControllerPublicationLifecycle.StandalonePublished)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -522,9 +629,23 @@ public sealed class PlayerMovementController
|
|||
PhysicsEngine physics,
|
||||
RetailObjectQuantumClock? objectClock,
|
||||
PlayerMovementConstructionOptions options)
|
||||
: this(
|
||||
physics,
|
||||
objectClock,
|
||||
options,
|
||||
PlayerMovementControllerPublicationLifecycle.StandalonePublished)
|
||||
{
|
||||
}
|
||||
|
||||
private PlayerMovementController(
|
||||
PhysicsEngine physics,
|
||||
RetailObjectQuantumClock? objectClock,
|
||||
PlayerMovementConstructionOptions options,
|
||||
PlayerMovementControllerPublicationLifecycle publicationLifecycle)
|
||||
{
|
||||
_physics = physics;
|
||||
_objectClock = objectClock ?? new RetailObjectQuantumClock();
|
||||
_publicationLifecycle = publicationLifecycle;
|
||||
|
||||
_body = new PhysicsBody
|
||||
{
|
||||
|
|
@ -551,8 +672,8 @@ public sealed class PlayerMovementController
|
|||
// R5-V5: the MovementManager facade owns the interp from birth
|
||||
// (retail CPhysicsObj::movement_manager); the moveto child binds
|
||||
// later via MoveToFactory (EnterPlayerModeNow / the test rigs).
|
||||
Movement = new AcDream.Core.Physics.Motion.MovementManager(_motion);
|
||||
Movement.ActivatePhysicsObject = ActivateFromMovement;
|
||||
_movementManager = new AcDream.Core.Physics.Motion.MovementManager(_motion);
|
||||
_movementManager.ActivatePhysicsObject = ActivateFromMovement;
|
||||
// R3-W4 (A3): the local player's movement is input-driven —
|
||||
// movement_is_autonomous true so apply_current_movement's dual
|
||||
// dispatch routes apply_raw_movement (IsThePlayer && autonomous).
|
||||
|
|
@ -560,6 +681,117 @@ public sealed class PlayerMovementController
|
|||
_body.LastMoveWasAutonomous = true;
|
||||
}
|
||||
|
||||
internal static PlayerMovementController CreatePublicationCandidate(
|
||||
PhysicsEngine physics,
|
||||
PlayerMovementConstructionOptions options) => new(
|
||||
physics,
|
||||
new RetailObjectQuantumClock(),
|
||||
options,
|
||||
PlayerMovementControllerPublicationLifecycle.CandidatePreparing);
|
||||
|
||||
internal PhysicsBody PhysicsBody
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
return _body;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool OwnsPhysicsBody(PhysicsBody body) =>
|
||||
ReferenceEquals(_body, body);
|
||||
|
||||
internal bool IsSealedPublicationCandidate => _publicationLifecycle
|
||||
is PlayerMovementControllerPublicationLifecycle.CandidateSealed;
|
||||
|
||||
internal void SealPublicationCandidate()
|
||||
{
|
||||
if (_publicationLifecycle
|
||||
is not PlayerMovementControllerPublicationLifecycle
|
||||
.CandidatePreparing)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Only a preparing Runtime movement candidate can be sealed.");
|
||||
}
|
||||
_publicationLifecycle = PlayerMovementControllerPublicationLifecycle
|
||||
.CandidateSealed;
|
||||
}
|
||||
|
||||
internal void CommitRuntimeOwnership(RetailObjectQuantumClock objectClock)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(objectClock);
|
||||
if (_publicationLifecycle
|
||||
is not PlayerMovementControllerPublicationLifecycle.CandidateSealed)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Only a sealed Runtime movement candidate can be published.");
|
||||
}
|
||||
_objectClock = objectClock;
|
||||
_publicationLifecycle = PlayerMovementControllerPublicationLifecycle
|
||||
.RuntimeOwnedDormant;
|
||||
}
|
||||
|
||||
internal void ActivateRuntimePublication()
|
||||
{
|
||||
if (_publicationLifecycle
|
||||
is not PlayerMovementControllerPublicationLifecycle
|
||||
.RuntimeOwnedDormant)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Only a dormant Runtime-owned movement controller can be activated.");
|
||||
}
|
||||
_publicationLifecycle = PlayerMovementControllerPublicationLifecycle
|
||||
.RuntimePublished;
|
||||
}
|
||||
|
||||
internal void DiscardRuntimeCandidate()
|
||||
{
|
||||
if (_publicationLifecycle
|
||||
is PlayerMovementControllerPublicationLifecycle.CandidatePreparing
|
||||
or PlayerMovementControllerPublicationLifecycle.CandidateSealed)
|
||||
{
|
||||
_publicationLifecycle = PlayerMovementControllerPublicationLifecycle
|
||||
.Discarded;
|
||||
}
|
||||
}
|
||||
|
||||
internal void RetireRuntimePublication()
|
||||
{
|
||||
if (_publicationLifecycle
|
||||
is PlayerMovementControllerPublicationLifecycle.RuntimeOwnedDormant
|
||||
or PlayerMovementControllerPublicationLifecycle.RuntimePublished)
|
||||
{
|
||||
_publicationLifecycle = PlayerMovementControllerPublicationLifecycle
|
||||
.RuntimeRetired;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureConfigurationMutable()
|
||||
{
|
||||
if (_publicationLifecycle
|
||||
is PlayerMovementControllerPublicationLifecycle.StandalonePublished
|
||||
or PlayerMovementControllerPublicationLifecycle
|
||||
.CandidatePreparing
|
||||
or PlayerMovementControllerPublicationLifecycle.RuntimePublished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
throw new InvalidOperationException(
|
||||
"A sealed, retired, or discarded Runtime movement controller cannot be mutated.");
|
||||
}
|
||||
|
||||
private void EnsurePublishedForRuntimeOperation()
|
||||
{
|
||||
if (_publicationLifecycle
|
||||
is PlayerMovementControllerPublicationLifecycle.StandalonePublished
|
||||
or PlayerMovementControllerPublicationLifecycle.RuntimePublished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
throw new InvalidOperationException(
|
||||
"An unpublished or retired Runtime movement controller cannot execute live movement operations.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Host half of retail <c>MovementManager::PerformMovement</c>'s
|
||||
/// unconditional <c>CPhysicsObj::set_active(1)</c> head. Static objects
|
||||
|
|
@ -568,6 +800,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
private void ActivateFromMovement()
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
if ((_body.State & PhysicsStateFlags.Static) != 0)
|
||||
return;
|
||||
|
||||
|
|
@ -582,6 +815,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
internal void SuspendObjectUpdate(float elapsedSeconds)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
AdvancedObjectQuantumLastTick = false;
|
||||
if (float.IsFinite(elapsedSeconds) && elapsedSeconds > 0f)
|
||||
_simTimeSeconds += elapsedSeconds;
|
||||
|
|
@ -596,6 +830,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public bool BeginMouseLook(MovementInput input)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
if (_mouseLookActive || State != PlayerState.InWorld)
|
||||
return false;
|
||||
|
||||
|
|
@ -620,6 +855,7 @@ public sealed class PlayerMovementController
|
|||
float signedAdjustment,
|
||||
MovementInput input)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
if (!_mouseLookActive || !float.IsFinite(signedAdjustment))
|
||||
return;
|
||||
|
||||
|
|
@ -637,6 +873,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public void StopMouseDrift(MovementInput input)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
if (!_mouseLookActive)
|
||||
return;
|
||||
|
||||
|
|
@ -662,6 +899,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public bool EndMouseLook(MovementInput input)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
if (!_mouseLookActive && !_activeInputTurnFromMouse)
|
||||
return false;
|
||||
|
||||
|
|
@ -874,6 +1112,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public bool PrepareForAttackRequest()
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
// CommandInterpreter::MaybeStopCompletely @ 0x006B3B90 is a no-op
|
||||
// while an authoritative server movement owns the player.
|
||||
if (_controlledByServer)
|
||||
|
|
@ -898,6 +1137,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public bool RequestPosture(uint motion)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
if (motion is not (
|
||||
MotionCommand.Ready
|
||||
or MotionCommand.Crouch
|
||||
|
|
@ -922,6 +1162,7 @@ public sealed class PlayerMovementController
|
|||
|
||||
public void SetCharacterSkills(int runSkill, int jumpSkill)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_weenie.SetSkills(runSkill, jumpSkill);
|
||||
}
|
||||
|
||||
|
|
@ -934,6 +1175,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public void SetCharacterBurden(float burden)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_weenie.SetBurden(burden);
|
||||
}
|
||||
|
||||
|
|
@ -946,6 +1188,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public void SetCharacterStamina(int currentStamina)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_weenie.SetStamina(currentStamina < 0 ? null : (uint)currentStamina);
|
||||
}
|
||||
|
||||
|
|
@ -960,6 +1203,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public void SetCharacterPkStatus(int playerKillerStatus, float? lastPkAttackTimestamp)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_weenie.SetPlayerKillerStatus(
|
||||
playerKillerStatus < 0 ? null : playerKillerStatus,
|
||||
lastPkAttackTimestamp);
|
||||
|
|
@ -972,7 +1216,14 @@ public sealed class PlayerMovementController
|
|||
/// path remotes use. R3-W6 widens this into the full local-player
|
||||
/// unification.
|
||||
/// </summary>
|
||||
internal MotionInterpreter Motion => _motion;
|
||||
internal MotionInterpreter Motion
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
return _motion;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CPhysicsObj::StopCompletely</c> (0x00510180) enters through
|
||||
|
|
@ -981,11 +1232,14 @@ public sealed class PlayerMovementController
|
|||
/// performs the required zero-duration animation completion sweep after
|
||||
/// the interpreter has queued its matching pending-motion node.
|
||||
/// </summary>
|
||||
internal WeenieError StopCompletelyAtPhysicsObjectBoundary() =>
|
||||
Movement.PerformMovement(new MovementStruct
|
||||
internal WeenieError StopCompletelyAtPhysicsObjectBoundary()
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
return _movementManager.PerformMovement(new MovementStruct
|
||||
{
|
||||
Type = MovementType.StopCompletely,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CPhysicsObj::DoMotion</c> (0x00510020) constructs a type-1
|
||||
|
|
@ -999,6 +1253,7 @@ public sealed class PlayerMovementController
|
|||
uint motion,
|
||||
AcDream.Core.Physics.Motion.MovementParameters parameters)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_body.LastMoveWasAutonomous = true;
|
||||
return Movement.PerformMovement(new MovementStruct
|
||||
{
|
||||
|
|
@ -1018,6 +1273,7 @@ public sealed class PlayerMovementController
|
|||
uint motion,
|
||||
AcDream.Core.Physics.Motion.MovementParameters parameters)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_body.LastMoveWasAutonomous = true;
|
||||
return Movement.PerformMovement(new MovementStruct
|
||||
{
|
||||
|
|
@ -1048,6 +1304,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public void SetBodyOrientation(Quaternion orientation)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_body.Orientation = AcDream.Core.Physics.Motion.FrameOps.SetRotate(
|
||||
_body.Position,
|
||||
_body.Orientation,
|
||||
|
|
@ -1066,6 +1323,7 @@ public sealed class PlayerMovementController
|
|||
/// dispatched motions with the idle raw state.</summary>
|
||||
internal void SetLastMoveWasAutonomous(bool autonomous)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_body.LastMoveWasAutonomous = autonomous;
|
||||
_controlledByServer = !autonomous;
|
||||
}
|
||||
|
|
@ -1093,6 +1351,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public void AttachCycleVelocityAccessor(Func<Vector3> accessor)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
if (accessor is null) throw new ArgumentNullException(nameof(accessor));
|
||||
_motion.GetCycleVelocity = accessor;
|
||||
}
|
||||
|
|
@ -1109,6 +1368,7 @@ public sealed class PlayerMovementController
|
|||
Action<float, AcDream.Core.Physics.Motion.MotionDeltaFrame> advance,
|
||||
Action? processHooks = null)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
_advanceAnimationRootMotion = advance
|
||||
?? throw new ArgumentNullException(nameof(advance));
|
||||
_processAnimationHooks = processHooks;
|
||||
|
|
@ -1122,6 +1382,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public void NoteMovementSent(float nowSeconds, bool mouseLookEvent = false)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
_lastSentTime = nowSeconds;
|
||||
if (mouseLookEvent)
|
||||
{
|
||||
|
|
@ -1137,6 +1398,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public MovementResult CaptureMovementResult(bool mouseLookEvent)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
var raw = _motion.RawState;
|
||||
uint? forward = raw.ForwardCommand == RawMotionState.Default.ForwardCommand
|
||||
? null : raw.ForwardCommand;
|
||||
|
|
@ -1189,6 +1451,7 @@ public sealed class PlayerMovementController
|
|||
System.Numerics.Plane contactPlane,
|
||||
float nowSeconds)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
_lastSentPosition = position;
|
||||
_lastSentContactPlane = contactPlane;
|
||||
_lastSentTime = nowSeconds;
|
||||
|
|
@ -1207,6 +1470,7 @@ public sealed class PlayerMovementController
|
|||
System.Numerics.Plane currentContactPlane,
|
||||
float nowSeconds)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
if (!_lastSentInitialized)
|
||||
return true;
|
||||
|
||||
|
|
@ -1266,11 +1530,14 @@ public sealed class PlayerMovementController
|
|||
/// directly via <c>SnapToCell</c> rather than delta-syncing through the setter.
|
||||
/// </summary>
|
||||
public void SetPosition(Vector3 pos, uint cellId, Vector3 cellLocal)
|
||||
=> SetPositionCore(
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
SetPositionCore(
|
||||
pos,
|
||||
cellId,
|
||||
cellLocal,
|
||||
publishSharedState: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a new local controller's canonical body pose without publishing
|
||||
|
|
@ -1281,15 +1548,19 @@ public sealed class PlayerMovementController
|
|||
internal void PreparePositionForCommit(
|
||||
Vector3 pos,
|
||||
uint cellId,
|
||||
Vector3 cellLocal) =>
|
||||
Vector3 cellLocal)
|
||||
{
|
||||
EnsureConfigurationMutable();
|
||||
SetPositionCore(
|
||||
pos,
|
||||
cellId,
|
||||
cellLocal,
|
||||
publishSharedState: false);
|
||||
}
|
||||
|
||||
internal void CommitPreparedPosition()
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
_physics.UpdatePlayerCurrCell(CellId);
|
||||
PositionManager?.UnStick();
|
||||
// #167 (Campaign P P5): mirrors the SetPositionCore teleport_hook
|
||||
|
|
@ -1404,6 +1675,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public void BlipPosition(Vector3 pos, uint cellId, Vector3 cellLocal)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
_body.SnapToCell(cellId, pos, cellLocal);
|
||||
_prevPhysicsPos = pos;
|
||||
_currPhysicsPos = pos;
|
||||
|
|
@ -1435,6 +1707,7 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public MovementResult TickHidden(float dt, Action? handleTargeting = null)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
AdvancedObjectQuantumLastTick = false;
|
||||
if (!float.IsFinite(dt) || dt <= 0f)
|
||||
{
|
||||
|
|
@ -1554,6 +1827,7 @@ public sealed class PlayerMovementController
|
|||
MovementInput input,
|
||||
Action? handleTargeting = null)
|
||||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
AdvancedObjectQuantumLastTick = false;
|
||||
// Reject a malformed host-frame duration at the controller boundary.
|
||||
// The retail object clock cannot sanitize state that input/jump/yaw
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
{
|
||||
private PlayerMovementController? _controller;
|
||||
private PlayerMovementController? _preparingMotionOwner;
|
||||
private RuntimeLocalPlayerPhysicsPublicationState? _physicsPublication;
|
||||
private bool _autoRunActive;
|
||||
private bool _hasCommandInput;
|
||||
private MovementInput _commandInput;
|
||||
|
|
@ -41,7 +42,9 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (ReferenceEquals(_controller, value))
|
||||
return;
|
||||
_controller?.RetireRuntimePublication();
|
||||
_controller = value;
|
||||
ControllerOwnershipEpoch++;
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,8 +53,13 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
public bool HasCommandInput => _hasCommandInput;
|
||||
public MovementInput CommandInput => _commandInput;
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
public ulong ControllerOwnershipEpoch { get; private set; }
|
||||
public IRuntimeMovementView View => this;
|
||||
|
||||
internal RuntimeLocalPlayerPhysicsPublicationState PhysicsPublication =>
|
||||
_physicsPublication ?? throw new InvalidOperationException(
|
||||
"The Runtime local-player physics publication owner is not bound.");
|
||||
|
||||
MotionInterpreter? IRuntimeLocalPlayerMotionSource.Motion =>
|
||||
_preparingMotionOwner?.Motion ?? _controller?.Motion;
|
||||
|
||||
|
|
@ -236,6 +244,7 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
public void ResetSession()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
_physicsPublication?.ResetSession();
|
||||
bool changed =
|
||||
_autoRunActive
|
||||
|| _hasCommandInput
|
||||
|
|
@ -244,7 +253,12 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
_autoRunActive = false;
|
||||
_hasCommandInput = false;
|
||||
_commandInput = default;
|
||||
_controller = null;
|
||||
if (_controller is not null)
|
||||
{
|
||||
_controller.RetireRuntimePublication();
|
||||
_controller = null;
|
||||
ControllerOwnershipEpoch++;
|
||||
}
|
||||
_preparingMotionOwner = null;
|
||||
if (changed)
|
||||
Interlocked.Increment(ref _revision);
|
||||
|
|
@ -257,7 +271,9 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
_preparingMotionOwner is not null,
|
||||
_autoRunActive,
|
||||
_hasCommandInput,
|
||||
Revision);
|
||||
Revision,
|
||||
ControllerOwnershipEpoch,
|
||||
_physicsPublication?.CaptureOwnership() ?? default);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
@ -266,12 +282,46 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
_autoRunActive = false;
|
||||
_hasCommandInput = false;
|
||||
_commandInput = default;
|
||||
_controller = null;
|
||||
_physicsPublication?.Dispose();
|
||||
if (_controller is not null)
|
||||
{
|
||||
_controller.RetireRuntimePublication();
|
||||
_controller = null;
|
||||
ControllerOwnershipEpoch++;
|
||||
}
|
||||
_preparingMotionOwner = null;
|
||||
Interlocked.Increment(ref _revision);
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
internal void AttachPhysicsPublication(
|
||||
RuntimeLocalPlayerPhysicsPublicationState publication)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(publication);
|
||||
if (_physicsPublication is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The Runtime local-player physics publication owner is already bound.");
|
||||
}
|
||||
_physicsPublication = publication;
|
||||
}
|
||||
|
||||
internal bool CanCommitRuntimeOwnedController(
|
||||
ulong expectedEpoch,
|
||||
PlayerMovementController? expectedController) =>
|
||||
!_disposed
|
||||
&& ControllerOwnershipEpoch == expectedEpoch
|
||||
&& ReferenceEquals(_controller, expectedController);
|
||||
|
||||
internal void CommitRuntimeOwnedController(PlayerMovementController controller)
|
||||
{
|
||||
_controller?.RetireRuntimePublication();
|
||||
_controller = controller;
|
||||
ControllerOwnershipEpoch++;
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
private void EndMotionPreparation(PlayerMovementController controller)
|
||||
{
|
||||
// Terminal disposal clears the unpublished construction seam. A
|
||||
|
|
@ -310,12 +360,15 @@ public readonly record struct RuntimeLocalMovementOwnershipSnapshot(
|
|||
bool HasPreparingMotionOwner,
|
||||
bool AutoRunActive,
|
||||
bool HasCommandInput,
|
||||
long Revision)
|
||||
long Revision,
|
||||
ulong ControllerOwnershipEpoch,
|
||||
RuntimeLocalPlayerPhysicsPublicationOwnershipSnapshot PhysicsPublication)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
&& !HasController
|
||||
&& !HasPreparingMotionOwner
|
||||
&& !AutoRunActive
|
||||
&& !HasCommandInput;
|
||||
&& !HasCommandInput
|
||||
&& PhysicsPublication.IsConverged;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -573,6 +573,30 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
|||
return RuntimeSetPositionMoverPreparationStatus.Prepared;
|
||||
}
|
||||
|
||||
internal bool IsExactPreparedPlacementCurrent(
|
||||
RuntimeEntityRecord record,
|
||||
in RuntimeEntityPlacementToken token,
|
||||
in RuntimeSetPositionCommand command)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
return token.IsValid
|
||||
&& token.Entity == record.Key
|
||||
&& _operations.TryGetValue(token.Entity, out Operation? operation)
|
||||
&& ReferenceEquals(operation.Record, record)
|
||||
&& operation.Token == token
|
||||
&& operation.Stage
|
||||
is RuntimeEntityPlacementStage.AwaitingPreparation
|
||||
&& IsCurrent(operation)
|
||||
&& _moverPreparationAuthorities.TryGetValue(
|
||||
token.Entity,
|
||||
out MoverPreparationAuthority authority)
|
||||
&& authority.OperationId == token.OperationId
|
||||
&& authority.Prepared
|
||||
&& authority.PreparedCommand == command
|
||||
&& IsPreparationAuthorityCurrent(operation, authority);
|
||||
}
|
||||
|
||||
internal RuntimeSetPositionOutcome SubmitPreparedPlacement(
|
||||
in RuntimeEntityPlacementToken token,
|
||||
in RuntimeSetPositionCommand command) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue