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,126 @@
# Runtime local-player physics publication - 2026-08-01
## Scope
This is placement Slice 4B2 checkpoint 4. It adds the dormant,
presentation-independent transaction which prepares and assigns ownership of
one local-player `PhysicsBody` and `PlayerMovementController`. No App or
Headless production route invokes this transaction, so graphical and no-window
game behavior is unchanged and AP-1/AD-1 remain open.
The checkpoint deliberately stops before canonical SetPosition activation.
It does not consume the prepared placement operation, enter the body into the
physics engine, publish FullCell/world/host/shadow/workset state, or project a
presentation entity. Those effects belong to the next transaction and must all
use the same Runtime-owned dormant body.
## Ownership contract
`RuntimeLocalPlayerPhysicsPublicationState` is the sole owner of unpublished
local-player body/controller candidates. Each candidate is bound to a token
containing:
- The exact `RuntimeEntityKey` and authored SetPosition placement token.
- A monotonic publication ID.
- The nonzero canonical local-player server GUID and exact identity revision.
- The record's physics-body and object-clock ownership epochs.
- The movement state's controller ownership epoch.
- The entity directory's session-lifetime authority.
Preparation constructs a private controller, body, and object clock. It applies
the exact authored cell frame, orientation, Setup sphere list, scale, step
heights, and accepted final physics state without mutating the canonical entity,
shared object clock, engine/worksets, shadow registry, FullCell, host state, or
presentation. The candidate remains explicitly out of world and inactive. No
method exposes its controller, body, clock, or another mutable reference while
it is owned by the publication transaction.
This checkpoint accepts only a pristine initial graph: no canonical body,
movement controller, physics host, remote motion, projectile, acquisition or
binding operation, or remote-placement contract may exist. It cannot replace or
upgrade a live graph. The local-player identity must be live, nonzero, and name
the same server GUID as the exact entity incarnation.
Unpublished candidates and ownership-committed dormant controllers reject live
movement operations: update, public SetPosition, blip, outbound-position
capture, movement/position send tracking, and shared-engine position commit.
Only the subsequent activation transaction may promote `RuntimeOwnedDormant`
to `RuntimePublished`; this checkpoint never invokes that transition. Once a
Runtime-owned dormant or published controller is replaced, reset, or disposed,
its terminal retirement state rejects the same operations plus
body/configuration mutation and manager acquisition. Publicly constructed
legacy controllers keep their existing standalone behavior.
## Failure-atomic commit
Commit revalidates every authority after preparation:
- The entity record is the current incarnation and is not accepted for delete.
- The local-player identity still has the token's exact GUID and revision and
has not been disposed.
- The exact authored SetPosition operation and sealed command remain current.
- Session, body, object-clock, and controller ownership epochs still match.
- The body/controller/host/remote/projectile graph remains completely pristine,
with no acquisition, binding, or remote-placement operation in progress.
Only after validation completes does the callback-free update-thread tail:
1. Rebind the candidate controller from its private clock to the record's exact
canonical object clock and mark it Runtime-owned but dormant.
2. Store the candidate's exact body on the canonical record, advancing the
physics ownership epoch once.
3. Store the same controller in `RuntimeLocalPlayerMovementState`, advancing the
controller ownership epoch once.
The dormant controller rejects every live/configuration operation after these
stores; ownership commit alone cannot tick physics, mutate the canonical clock,
or publish an outbound frame. These stores allocate no new gameplay owner,
invoke no host or presentation callback, and cannot replay an older
incarnation. Replacing SetPosition,
changing any accepted physics authority, binding remote/projectile state,
replacing body/clock/controller ownership, delete plus GUID reuse, reset, or
disposal causes the token to reject. An identity switch away and back also
rejects because its revision changed. A rejected or superseded candidate is
discarded and cannot perform a later live operation. Reset and disposal converge
the publication ledger to zero candidates.
Repeated stores of the same body/controller do not advance their epochs; real
bind, replacement, and unbind edges do. This makes ABA-shaped reference changes
observable even if a later value happens to equal an earlier reference.
## Gates
- Candidate privacy and live-operation rejection.
- Pristine-only admission for body, controller, host, remote/projectile,
acquisition/binding, and remote-placement ownership.
- Exact local-player identity, identity-switch, and disposed-identity rejection.
- Exact same-body ownership in entity record and dormant movement controller.
- Dormant rejection after ownership commit plus the isolated controller-level
`dormant -> activated -> live` lifecycle contract for the next checkpoint.
- No mutation of SetPosition, FullCell, spatial roots, host projections,
shadows, worksets, world residence, or presentation during this checkpoint.
- Replacement by position, vector, final physics state, object description,
CreateObject, remote/projectile/body/clock/controller ownership, and explicit
placement cancellation.
- Delete plus same-GUID reincarnation.
- Candidate replacement, reset, disposal, and ownership convergence.
- Terminal stale-controller rejection after replacement, reset, and disposal.
- Body/controller epochs advance only on actual ownership changes.
Focused publication, controller, movement, and SetPosition tests pass 167/167.
The complete Runtime project passes 627/627 under invariant culture. The App
Runtime-ownership guard passes 4/4, the complete Release solution builds with
zero errors, and the complete Release solution test gate passes 10,374 tests
with 4 intentional skips. Under the machine's Swedish current culture, the
three previously known formatting assertions still fail (`0,5` versus `0.5`
and localized sky text); they are unrelated to this checkpoint.
## Next checkpoint
Add the canonical Runtime SetPosition activation transaction. It must evaluate
and commit the already-owned dormant body, consume the exact prepared placement,
and atomically establish physics-engine/workset/shadow/FullCell/world/host
ownership before presentation receives an acknowledgement, then invoke the sole
`ActivateRuntimePublication` transition. The activation must
roll back or leave the operation retryable on every pre-commit failure and must
not construct a second body or controller.

View file

@ -342,7 +342,7 @@ public sealed class RuntimeEntityDirectory
AcDream.Core.Physics.PhysicsBody? body)
{
EnsureKnown(record);
record.PhysicsBody = body;
record.SetPhysicsBody(body);
}
public void SetPhysicsBodyAcquisitionInProgress(

View file

@ -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

View file

@ -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; }

View file

@ -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

View file

@ -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;
}

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();
}
}

View file

@ -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) =>

View file

@ -1100,6 +1100,74 @@ public class PlayerMovementControllerTests
public bool StopMotion(uint motion) => true;
}
[Fact]
public void PublicationLifecycleRequiresExplicitActivationAfterOwnershipCommit()
{
var candidate = PlayerMovementController.CreatePublicationCandidate(
new PhysicsEngine(),
PlayerMovementConstructionOptions.Fallback);
candidate.LocalEntityId = 0x70004001u;
candidate.StepUpHeight = 0.4f;
candidate.StepDownHeight = 0.4f;
candidate.ObjectScale = 1f;
candidate.PreparePositionForCommit(
new Vector3(1f, 2f, 3f),
0xA9B40021u,
new Vector3(1f, 2f, 3f));
candidate.SetBodyOrientation(Quaternion.Identity);
candidate.ApplyPhysicsState(PhysicsStateFlags.Gravity);
PhysicsBody body = candidate.PhysicsBody;
body.InWorld = false;
body.TransientState &= ~TransientStateFlags.Active;
Assert.False(body.InWorld);
candidate.SealPublicationCandidate();
Assert.True(candidate.IsSealedPublicationCandidate);
Assert.Throws<InvalidOperationException>(() => candidate.Update(
1f / 60f,
default));
Assert.Throws<InvalidOperationException>(() => candidate.TickHidden(
1f / 60f));
Assert.Throws<InvalidOperationException>(() => candidate.SetPosition(
Vector3.One,
0xA9B40021u,
Vector3.One));
Assert.Throws<InvalidOperationException>(() => candidate.BlipPosition(
Vector3.One,
0xA9B40021u,
Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
candidate.CaptureMovementResult(mouseLookEvent: false));
Assert.Throws<InvalidOperationException>(() =>
candidate.NoteMovementSent(1f));
Assert.Throws<InvalidOperationException>(candidate.CommitPreparedPosition);
Assert.Throws<InvalidOperationException>(() =>
candidate.ApplyPhysicsState(PhysicsStateFlags.Frozen));
Assert.Throws<InvalidOperationException>(() => candidate.LocalEntityId = 2u);
Assert.Throws<InvalidOperationException>(() => _ = candidate.Movement);
var canonicalClock = new RetailObjectQuantumClock();
candidate.CommitRuntimeOwnership(canonicalClock);
Assert.Throws<InvalidOperationException>(() => candidate.Update(
1f / 60f,
default));
Assert.Throws<InvalidOperationException>(() =>
candidate.CaptureMovementResult(mouseLookEvent: false));
Assert.Throws<InvalidOperationException>(() => _ = candidate.PhysicsBody);
candidate.ActivateRuntimePublication();
Assert.Same(body, candidate.PhysicsBody);
_ = candidate.CaptureMovementResult(mouseLookEvent: false);
_ = candidate.Update(0f, default);
candidate.ApplyPhysicsState(PhysicsStateFlags.Gravity);
candidate.RetireRuntimePublication();
Assert.Throws<InvalidOperationException>(() => candidate.Update(
1f / 60f,
default));
}
[Fact]
public void Update_RunningJumpLandsOnFlatGround_ResidualVelocitySurvivesAndDecays_NotFrozen()
{

View file

@ -0,0 +1,713 @@
using System.Collections.Immutable;
using System.Numerics;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Tests.Gameplay;
public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
{
private const uint Cell = 0xA9B40021u;
private const uint SetupId = 0x02000001u;
[Fact]
public void PreparationOwnsPrivateBodyAndMutatesNoCanonicalOrSharedState()
{
using var fixture = new Fixture();
uint fullCell = fixture.Record.FullCellId;
ulong spatial = fixture.Record.SpatialAuthorityVersion;
ulong physicsEpoch = fixture.Record.PhysicsOwnershipEpoch;
ulong clockEpoch = fixture.Record.ObjectClockEpoch;
RuntimePhysicsOwnershipSnapshot physics = fixture.Lifetime.Physics
.CaptureOwnership();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(fullCell, fixture.Record.FullCellId);
Assert.Equal(spatial, fixture.Record.SpatialAuthorityVersion);
Assert.Equal(physicsEpoch, fixture.Record.PhysicsOwnershipEpoch);
Assert.Equal(clockEpoch, fixture.Record.ObjectClockEpoch);
Assert.Null(fixture.Record.PhysicsHost);
Assert.Equal(physics.SpatialRootCount,
fixture.Lifetime.Physics.CaptureOwnership().SpatialRootCount);
Assert.Equal(physics.RetainedShadowRegistrationCount,
fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.Equal(1, fixture.Owner.CaptureOwnership().CandidateCount);
Assert.True(token.IsValid);
}
[Fact]
public void CommitOwnsExactDormantBodyAndControllerWithoutWorldEdges()
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
Assert.True(fixture.Owner.TryCaptureCandidateSnapshot(
token,
out RuntimeLocalPlayerPhysicsCandidateSnapshot prepared));
uint fullCell = fixture.Record.FullCellId;
ulong spatial = fixture.Record.SpatialAuthorityVersion;
ulong bodyEpoch = fixture.Record.PhysicsOwnershipEpoch;
ulong controllerEpoch = fixture.Movement.ControllerOwnershipEpoch;
double clockPending = fixture.Record.ObjectClock.PendingSeconds;
bool clockActive = fixture.Record.ObjectClock.IsActive;
object? currentCell = fixture.Lifetime.Physics.DataCache.CellGraph.CurrCell;
RuntimeLocalPlayerPhysicsPublicationStatus status = fixture.Owner
.Commit(token);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed, status);
PlayerMovementController candidate = Assert.IsType<PlayerMovementController>(
fixture.Movement.Controller);
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.Same(body, fixture.Record.PhysicsBody);
Assert.Same(candidate, fixture.Movement.Controller);
Assert.True(candidate.OwnsPhysicsBody(body));
Assert.Equal(bodyEpoch + 1UL, fixture.Record.PhysicsOwnershipEpoch);
Assert.Equal(controllerEpoch + 1UL,
fixture.Movement.ControllerOwnershipEpoch);
Assert.Equal(fullCell, fixture.Record.FullCellId);
Assert.Equal(spatial, fixture.Record.SpatialAuthorityVersion);
Assert.Same(currentCell,
fixture.Lifetime.Physics.DataCache.CellGraph.CurrCell);
Assert.Equal(clockPending, fixture.Record.ObjectClock.PendingSeconds);
Assert.Equal(clockActive, fixture.Record.ObjectClock.IsActive);
Assert.Equal(prepared.Position, body.Position);
Assert.Equal(prepared.Orientation, body.Orientation);
Assert.Equal(prepared.CellId, body.CellPosition.ObjCellId);
Assert.Equal(prepared.CellLocalPosition,
body.CellPosition.Frame.Origin);
Assert.Equal(prepared.State, body.State);
Assert.Equal(prepared.TransientState, body.TransientState);
Assert.Equal(prepared.InWorld, body.InWorld);
Assert.False(body.InWorld);
Assert.False((body.TransientState & TransientStateFlags.Active) != 0);
Assert.Null(fixture.Record.PhysicsHost);
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.SpatialRootCount);
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.True(fixture.Lifetime.Physics.SetPosition
.IsExactPreparedPlacementCurrent(
fixture.Record,
fixture.Placement,
fixture.Command));
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
AssertNotLive(candidate);
Assert.Equal(prepared.Position, body.Position);
Assert.Equal(prepared.Orientation, body.Orientation);
Assert.Equal(prepared.State, body.State);
Assert.Equal(prepared.TransientState, body.TransientState);
Assert.Equal(clockPending, fixture.Record.ObjectClock.PendingSeconds);
Assert.Equal(clockActive, fixture.Record.ObjectClock.IsActive);
Assert.Same(currentCell,
fixture.Lifetime.Physics.DataCache.CellGraph.CurrCell);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedToken,
fixture.Owner.Commit(token));
}
[Theory]
[InlineData(PublicationInvalidation.SetPositionReplacement)]
[InlineData(PublicationInvalidation.Vector)]
[InlineData(PublicationInvalidation.FinalState)]
[InlineData(PublicationInvalidation.ObjectDescription)]
[InlineData(PublicationInvalidation.Create)]
[InlineData(PublicationInvalidation.Remote)]
[InlineData(PublicationInvalidation.Projectile)]
[InlineData(PublicationInvalidation.Body)]
[InlineData(PublicationInvalidation.Clock)]
[InlineData(PublicationInvalidation.Controller)]
[InlineData(PublicationInvalidation.CancelPlacement)]
public void EveryNestedAuthorityReplacementDiscardsCandidateWithoutRollback(
PublicationInvalidation invalidation)
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
PhysicsBody? replacementBody = null;
PlayerMovementController? replacementController = null;
switch (invalidation)
{
case PublicationInvalidation.SetPositionReplacement:
Assert.True(fixture.Lifetime.Physics.SetPosition
.BeginAuthoredPlacement(
fixture.Record,
fixture.Record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative)
.IsValid);
break;
case PublicationInvalidation.Vector:
fixture.Lifetime.Entities.AdvanceVectorAuthority(fixture.Record);
break;
case PublicationInvalidation.FinalState:
fixture.Lifetime.Entities.SetFinalPhysicsState(
fixture.Record,
fixture.Record.FinalPhysicsState | PhysicsStateFlags.Frozen);
break;
case PublicationInvalidation.ObjectDescription:
fixture.Lifetime.Entities.AdvanceObjDescAuthority(fixture.Record);
break;
case PublicationInvalidation.Create:
fixture.Lifetime.Entities.AdvanceCreateAuthority(fixture.Record);
break;
case PublicationInvalidation.Remote:
fixture.Lifetime.Entities.SetRemoteMotion(
fixture.Record,
new RemoteMotion());
break;
case PublicationInvalidation.Projectile:
fixture.Lifetime.Entities.SetProjectile(
fixture.Record,
new RuntimeProjectile(
new PhysicsBody(),
new ProjectileCollisionSphere(Vector3.Zero, 0.1f)));
break;
case PublicationInvalidation.Body:
replacementBody = new PhysicsBody();
fixture.Lifetime.Entities.SetPhysicsBody(
fixture.Record,
replacementBody);
break;
case PublicationInvalidation.Clock:
fixture.Lifetime.Entities.SuspendObjectClock(fixture.Record);
break;
case PublicationInvalidation.Controller:
replacementController = new PlayerMovementController(
new PhysicsEngine());
fixture.Movement.Controller = replacementController;
break;
case PublicationInvalidation.CancelPlacement:
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
fixture.Record,
publishWithdrawal: false));
break;
default:
throw new ArgumentOutOfRangeException(nameof(invalidation));
}
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Commit(token));
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
if (replacementBody is not null)
Assert.Same(replacementBody, fixture.Record.PhysicsBody);
if (replacementController is not null)
Assert.Same(replacementController, fixture.Movement.Controller);
}
[Fact]
public void DeleteAndGuidReuseCannotPublishRetiredCandidate()
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
uint guid = fixture.Record.ServerGuid;
Assert.True(fixture.Lifetime.TryAcceptDelete(
new DeleteObject.Parsed(guid, fixture.Record.Incarnation),
isLocalPlayer: false,
removeRetainedObject: false,
out RuntimeEntityDeleteAcceptance acceptance));
fixture.Lifetime.CompleteAcceptedDelete(acceptance);
Assert.Null(fixture.Lifetime.RetireCanonicalOnly(fixture.Record));
RuntimeEntityRecord replacement = fixture.Lifetime.RegisterEntity(
Spawn(guid, incarnation: 2)).Canonical!;
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Commit(token));
Assert.Null(replacement.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
}
[Fact]
public void ResetAndDisposeDiscardCandidatesAndConvergeOwnership()
{
var fixture = new Fixture();
_ = fixture.Prepare();
fixture.Movement.ResetSession();
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
fixture.RepreparePlacement();
_ = fixture.Prepare();
fixture.Movement.Dispose();
Assert.True(fixture.Movement.CaptureOwnership().IsConverged);
fixture.DisposeLifetimeOnly();
}
[Theory]
[InlineData(PristineViolation.Body)]
[InlineData(PristineViolation.Controller)]
[InlineData(PristineViolation.Host)]
[InlineData(PristineViolation.BodyAcquisition)]
[InlineData(PristineViolation.Remote)]
[InlineData(PristineViolation.RemoteBinding)]
[InlineData(PristineViolation.Projectile)]
[InlineData(PristineViolation.ProjectileBinding)]
[InlineData(PristineViolation.RemotePlacement)]
public void PreparationRejectsEveryNonPristineOwnershipGraph(
PristineViolation violation)
{
using var fixture = new Fixture();
switch (violation)
{
case PristineViolation.Body:
{
var activeBody = new PhysicsBody
{
TransientState = TransientStateFlags.Active,
};
fixture.Lifetime.Entities.SetPhysicsBody(
fixture.Record,
activeBody);
break;
}
case PristineViolation.Controller:
fixture.Movement.Controller = new PlayerMovementController(
new PhysicsEngine());
break;
case PristineViolation.Host:
fixture.Lifetime.Entities.SetPhysicsHost(
fixture.Record,
CreatePhysicsHost(fixture.Record.ServerGuid));
break;
case PristineViolation.BodyAcquisition:
fixture.Lifetime.Entities.SetPhysicsBodyAcquisitionInProgress(
fixture.Record,
true);
break;
case PristineViolation.Remote:
fixture.Lifetime.Entities.SetRemoteMotion(
fixture.Record,
new RemoteMotion());
break;
case PristineViolation.RemoteBinding:
fixture.Lifetime.Entities.SetRemoteMotionBindingInProgress(
fixture.Record,
true);
break;
case PristineViolation.Projectile:
fixture.Lifetime.Entities.SetProjectile(
fixture.Record,
new RuntimeProjectile(
new PhysicsBody(),
new ProjectileCollisionSphere(Vector3.Zero, 0.1f)));
break;
case PristineViolation.ProjectileBinding:
fixture.Lifetime.Entities.SetProjectileBindingInProgress(
fixture.Record,
true);
break;
case PristineViolation.RemotePlacement:
fixture.Lifetime.Entities.SetRequiresRemotePlacementRuntime(
fixture.Record,
true);
break;
default:
throw new ArgumentOutOfRangeException(nameof(violation));
}
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
out RuntimeLocalPlayerPhysicsPublicationToken token));
Assert.False(token.IsValid);
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
}
[Fact]
public void PreparationRejectsRecordWhichIsNotTheExactLocalIdentity()
{
using var fixture = new Fixture();
fixture.Identity.ServerGuid = 0x70003002u;
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
out _));
}
[Fact]
public void PreparationRejectsDisposedLocalIdentity()
{
using var fixture = new Fixture();
fixture.Identity.Dispose();
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
out _));
}
[Fact]
public void IdentityRevisionSwitchRejectsPreparedCandidate()
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
uint guid = fixture.Identity.ServerGuid;
fixture.Identity.ServerGuid = guid + 1u;
fixture.Identity.ServerGuid = guid;
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Commit(token));
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
}
[Theory]
[InlineData(PublishedRetirement.Replacement)]
[InlineData(PublishedRetirement.Reset)]
[InlineData(PublishedRetirement.Dispose)]
public void RuntimeOwnedControllerRejectsStaleOperationsAfterRetirement(
PublishedRetirement retirement)
{
using var fixture = new Fixture();
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare()));
PlayerMovementController stale = fixture.Movement.Controller!;
switch (retirement)
{
case PublishedRetirement.Replacement:
fixture.Movement.Controller = new PlayerMovementController(
new PhysicsEngine());
break;
case PublishedRetirement.Reset:
fixture.Movement.ResetSession();
break;
case PublishedRetirement.Dispose:
fixture.Movement.Dispose();
break;
default:
throw new ArgumentOutOfRangeException(nameof(retirement));
}
AssertNotLive(stale);
}
[Fact]
public void BodyAndControllerEpochsAdvanceOnlyOnOwnershipChanges()
{
using var fixture = new Fixture(preparePlacement: false);
var firstBody = new PhysicsBody();
var secondBody = new PhysicsBody();
ulong bodyEpoch = fixture.Record.PhysicsOwnershipEpoch;
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, firstBody);
Assert.Equal(bodyEpoch + 1UL, fixture.Record.PhysicsOwnershipEpoch);
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, firstBody);
Assert.Equal(bodyEpoch + 1UL, fixture.Record.PhysicsOwnershipEpoch);
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, secondBody);
Assert.Equal(bodyEpoch + 2UL, fixture.Record.PhysicsOwnershipEpoch);
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, null);
Assert.Equal(bodyEpoch + 3UL, fixture.Record.PhysicsOwnershipEpoch);
var first = new PlayerMovementController(new PhysicsEngine());
var second = new PlayerMovementController(new PhysicsEngine());
ulong controllerEpoch = fixture.Movement.ControllerOwnershipEpoch;
fixture.Movement.Controller = first;
Assert.Equal(controllerEpoch + 1UL,
fixture.Movement.ControllerOwnershipEpoch);
fixture.Movement.Controller = first;
Assert.Equal(controllerEpoch + 1UL,
fixture.Movement.ControllerOwnershipEpoch);
fixture.Movement.Controller = second;
Assert.Equal(controllerEpoch + 2UL,
fixture.Movement.ControllerOwnershipEpoch);
fixture.Movement.Controller = null;
Assert.Equal(controllerEpoch + 3UL,
fixture.Movement.ControllerOwnershipEpoch);
}
private sealed class Fixture : IDisposable
{
private bool _lifetimeDisposed;
internal Fixture(bool preparePlacement = true)
{
Lifetime = new RuntimeEntityObjectLifetime();
Movement = new RuntimeLocalPlayerMovementState();
Identity = new RuntimeLocalPlayerIdentityState();
Owner = new RuntimeLocalPlayerPhysicsPublicationState(
Lifetime.Entities,
Lifetime.Physics,
Movement,
Identity);
Movement.AttachPhysicsPublication(Owner);
Record = Lifetime.RegisterEntity(
Spawn(0x70003001u, incarnation: 1)).Canonical!;
Identity.ServerGuid = Record.ServerGuid;
if (preparePlacement)
RepreparePlacement();
}
internal RuntimeEntityObjectLifetime Lifetime { get; }
internal RuntimeLocalPlayerMovementState Movement { get; }
internal RuntimeLocalPlayerIdentityState Identity { get; }
internal RuntimeLocalPlayerPhysicsPublicationState Owner { get; }
internal RuntimeEntityRecord Record { get; }
internal RuntimeEntityPlacementToken Placement { get; private set; }
internal RuntimeSetPositionCommand Command { get; private set; }
internal void RepreparePlacement()
{
Placement = Lifetime.Physics.SetPosition.BeginAuthoredPlacement(
Record,
Record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative);
Assert.True(Placement.IsValid);
var setup = new FlatSetupCollision(
ImmutableArray<FlatCollisionCylinder>.Empty,
[new FlatCollisionSphere(Vector3.Zero, 0.48f)],
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f);
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
Lifetime.Physics.SetPosition.PrepareMover(
Placement,
new RuntimeSetPositionMoverPreparation(
RuntimeSetPositionMoverSetup.Resolved(SetupId, setup),
RuntimeSetPositionOperationKind.LocalAuthoritative,
GameTime: 10d,
PhysicsPlacementClass.Ordinary,
PhysicsSetPositionFlags.Placement
| PhysicsSetPositionFlags.Slide),
out RuntimeSetPositionCommand command));
Command = command;
}
internal RuntimeLocalPlayerPhysicsPublicationToken Prepare()
{
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Prepared,
Owner.Prepare(
Record,
Placement,
Command,
PlayerMovementConstructionOptions.Fallback,
out RuntimeLocalPlayerPhysicsPublicationToken token));
Assert.Equal(Record.Key, token.Entity);
Assert.Equal(Placement, token.Placement);
Assert.Equal(Identity.ServerGuid, token.LocalPlayerServerGuid);
Assert.Equal(Identity.Revision, token.LocalPlayerIdentityRevision);
return token;
}
internal void DisposeLifetimeOnly()
{
if (_lifetimeDisposed)
return;
Lifetime.Dispose();
_lifetimeDisposed = true;
}
public void Dispose()
{
Movement.Dispose();
Identity.Dispose();
DisposeLifetimeOnly();
}
}
private static WorldSession.EntitySpawn Spawn(uint guid, ushort incarnation)
{
var position = new CreateObject.ServerPosition(
Cell,
1f,
2f,
3f,
1f,
0f,
0f,
0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: incarnation);
var physics = new PhysicsSpawnData(
RawState: (uint)(PhysicsStateFlags.Gravity
| PhysicsStateFlags.ReportCollisions),
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: SetupId,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: 1f,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
Guid: guid,
Position: position,
SetupTableId: SetupId,
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: 1f,
Name: "local-publication-fixture",
ItemType: null,
MotionState: null,
MotionTableId: 0x09000001u,
PhysicsState: physics.RawState,
ObjectDescriptionFlags: 0x8u,
InstanceSequence: incarnation,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
public enum PublicationInvalidation
{
SetPositionReplacement,
Vector,
FinalState,
ObjectDescription,
Create,
Remote,
Projectile,
Body,
Clock,
Controller,
CancelPlacement,
}
public enum PristineViolation
{
Body,
Controller,
Host,
BodyAcquisition,
Remote,
RemoteBinding,
Projectile,
ProjectileBinding,
RemotePlacement,
}
public enum PublishedRetirement
{
Replacement,
Reset,
Dispose,
}
private static EntityPhysicsHost CreatePhysicsHost(uint id) => new(
id,
getPosition: static () => default,
getVelocity: static () => Vector3.Zero,
getRadius: static () => 0.48f,
inContact: static () => true,
minterpMaxSpeed: static () => null,
curTime: static () => 0d,
physicsTimerTime: static () => 0d,
getObjectA: static _ => null,
handleUpdateTarget: static _ => { },
interruptCurrentMovement: static () => { });
private static void AssertNotLive(PlayerMovementController controller)
{
Assert.Throws<InvalidOperationException>(() => controller.Update(
1f / 60f,
default));
Assert.Throws<InvalidOperationException>(() => controller.TickHidden(
1f / 60f));
Assert.Throws<InvalidOperationException>(() =>
controller.SuspendObjectUpdate(1f / 60f));
Assert.Throws<InvalidOperationException>(() => controller.SetPosition(
Vector3.One,
Cell,
Vector3.One));
Assert.Throws<InvalidOperationException>(() => controller.BlipPosition(
Vector3.One,
Cell,
Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
controller.ApplyPhysicsState(PhysicsStateFlags.Frozen));
Assert.Throws<InvalidOperationException>(() => controller.Yaw = 1f);
Assert.Throws<InvalidOperationException>(() =>
controller.CaptureMovementResult(mouseLookEvent: false));
Assert.Throws<InvalidOperationException>(() =>
controller.CapturePresentationResult());
Assert.Throws<InvalidOperationException>(() =>
controller.TryGetOutboundPosition(out _));
Assert.Throws<InvalidOperationException>(() =>
controller.NoteMovementSent(1f));
Assert.Throws<InvalidOperationException>(() =>
controller.NotePositionSent(default, default, 1f));
Assert.Throws<InvalidOperationException>(() =>
controller.ShouldSendPositionEvent(default, default, 1f));
Assert.Throws<InvalidOperationException>(() =>
controller.BeginMouseLook(default));
Assert.Throws<InvalidOperationException>(() =>
controller.SubmitMouseTurnAdjustment(1f, default));
Assert.Throws<InvalidOperationException>(() =>
controller.StopMouseDrift(default));
Assert.Throws<InvalidOperationException>(() =>
controller.EndMouseLook(default));
Assert.Throws<InvalidOperationException>(() =>
controller.PrepareForAttackRequest());
Assert.Throws<InvalidOperationException>(() =>
controller.RequestPosture(MotionCommand.Ready));
Assert.Throws<InvalidOperationException>(() =>
controller.CommitPreparedPosition());
Assert.Throws<InvalidOperationException>(() =>
controller.PreparePositionForCommit(Vector3.One, Cell, Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
controller.SetCharacterSkills(1, 1));
Assert.Throws<InvalidOperationException>(() =>
controller.SetBodyOrientation(Quaternion.Identity));
Assert.Throws<InvalidOperationException>(() =>
controller.SetLastMoveWasAutonomous(true));
Assert.Throws<InvalidOperationException>(() =>
controller.State = PlayerState.PortalSpace);
Assert.Throws<InvalidOperationException>(() =>
controller.StepUpHeight = 1f);
Assert.Throws<InvalidOperationException>(() =>
controller.AttachCycleVelocityAccessor(static () => Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
controller.AttachAnimationRootMotionSource(static (_, _) => { }));
Assert.Throws<InvalidOperationException>(() => _ = controller.Movement);
Assert.Throws<InvalidOperationException>(() => _ = controller.MoveTo);
Assert.Throws<InvalidOperationException>(() => _ = controller.PositionManager);
Assert.Throws<InvalidOperationException>(() => _ = controller.Motion);
Assert.Throws<InvalidOperationException>(() => _ = controller.PhysicsBody);
}
}