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