feat(runtime): prepare authored SetPosition movers
This commit is contained in:
parent
237d1184d2
commit
442cb8f97b
8 changed files with 1516 additions and 70 deletions
|
|
@ -159,14 +159,16 @@ the pre-existing line-ending/stat-noise paths are deliberately excluded.
|
||||||
|
|
||||||
## Remaining work - required order
|
## Remaining work - required order
|
||||||
|
|
||||||
### 1. Exact authored mover preparation
|
### 1. Exact authored mover preparation - complete 2026-08-01
|
||||||
|
|
||||||
Build every SetPosition request from Setup's ordered authored spheres, exact
|
The dormant preparation contract is implemented and independently reviewed.
|
||||||
scale/presence semantics, StepUp/StepDown heights, flags, cell-local frame and
|
It binds the Runtime-owned accepted frame and exact Setup DID, preserves the
|
||||||
orientation, and current position/vector/state authority versions. Do not
|
ordered authored spheres and scale/step semantics, seals the returned command,
|
||||||
synthesize a cylinder from visual radius/height or pre-mutate canonical state.
|
and forces stale deferred residents through exact re-preparation without
|
||||||
|
pre-mutating canonical state. See
|
||||||
|
[`2026-08-01-runtime-set-position-authored-mover-preparation.md`](2026-08-01-runtime-set-position-authored-mover-preparation.md).
|
||||||
|
|
||||||
### 2. Atomic local-controller/body publication
|
### 2. Atomic local-controller/body publication - next
|
||||||
|
|
||||||
Prepare off-canonical, then perform one Runtime-validated atomic transaction
|
Prepare off-canonical, then perform one Runtime-validated atomic transaction
|
||||||
which publishes the exact same body to graphical and no-window controllers.
|
which publishes the exact same body to graphical and no-window controllers.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
# Runtime SetPosition authored mover preparation - 2026-08-01
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This is placement Slice 4B2 checkpoint 3. It adds the dormant,
|
||||||
|
presentation-independent preparation contract used to turn an accepted Runtime
|
||||||
|
position into retail's exact `CPhysicsObj::SetPosition` mover input. No App or
|
||||||
|
Headless production route consumes the contract yet, so game behavior is
|
||||||
|
unchanged and AP-1/AD-1 remain open.
|
||||||
|
|
||||||
|
## Retail oracle
|
||||||
|
|
||||||
|
The implementation was checked against the named September 2013 client:
|
||||||
|
|
||||||
|
- `PhysicsDesc::PhysicsDesc` `0x0051D4D0`
|
||||||
|
- `PhysicsDesc::UnPack` `0x0051DDD0`
|
||||||
|
- `CPhysicsObj::set_description` `0x00514F40`
|
||||||
|
- `CPhysicsObj::SetPosition` `0x005160C0`
|
||||||
|
- `SPHEREPATH::init_sphere` `0x0050C670`
|
||||||
|
- `CPartArray::GetNumSphere` `0x00518060`
|
||||||
|
- `CPartArray::GetSphere` `0x00518070`
|
||||||
|
- `CPartArray::GetStepUpHeight` `0x005180D0`
|
||||||
|
- `CPartArray::GetStepDownHeight` `0x005180F0`
|
||||||
|
- `CTransition::init_object` `0x00509E40`
|
||||||
|
- `OBJECTINFO::init` `0x0050CF30`
|
||||||
|
|
||||||
|
SetPosition calls `CTransition::init_object(..., state = 0)` directly. It does
|
||||||
|
not use the ordinary-movement `CPhysicsObj::get_object_info` path. Consequently
|
||||||
|
the SetPosition state carries the player/PK/PKLite/impenetrable classifications
|
||||||
|
(plus acdream's pointer-free entry-restriction carrier), but it does not add
|
||||||
|
Contact, OnWalkable, PathClipped, FreeRotate, or EdgeSlide. Ethereal and
|
||||||
|
`step_down = !Missile` are separate `OBJECTINFO` fields derived from the current
|
||||||
|
physics state.
|
||||||
|
|
||||||
|
## Exact preparation contract
|
||||||
|
|
||||||
|
- Runtime captures the complete accepted server frame under the exact entity,
|
||||||
|
session, position, vector/velocity, wire-state, final-physics-state mutation,
|
||||||
|
object-description, and create-integration authorities. A host cannot
|
||||||
|
substitute a second position.
|
||||||
|
- Collision-world X/Y uses the target landblock's active live-centered offsets;
|
||||||
|
full cell ID, cell-local XYZ, and the complete quaternion remain unchanged.
|
||||||
|
- Setup resolution is bound to the canonical Setup DID. A known but unavailable
|
||||||
|
Setup remains retryable. Resolved-absent is valid only when the canonical
|
||||||
|
object has no Setup. An authored empty Setup remains distinct and still
|
||||||
|
contributes its scaled StepUp/StepDown heights.
|
||||||
|
- The complete ordered Setup sphere list is retained. Core later applies
|
||||||
|
retail's `min(count, 2)` traversal cap. The successfully resolved no-PartArray
|
||||||
|
or zero-sphere arm reaches Core with an empty list, where SetPosition supplies
|
||||||
|
the retail dummy sphere `(0,0,0.1)`, radius `0.1`, scale `1.0`.
|
||||||
|
- Scale precedence is `PhysicsDesc.Scale ?? EntitySpawn.ObjScale ?? 1.0`.
|
||||||
|
Present zero and finite negative values are preserved. Scale is not consumed
|
||||||
|
by a resolved-absent dummy mover.
|
||||||
|
- Every authored command is sealed to the exact preparation operation. Manual,
|
||||||
|
stale, replaced, or merely value-equivalent commands cannot bypass the seal.
|
||||||
|
- A wire-state, final-state mutation (including NoDraw and missile-stop),
|
||||||
|
vector/velocity, description, or create change during a deferred cell wait
|
||||||
|
returns the resident to `AwaitingPreparation`; the stale mover is never
|
||||||
|
replayed when the collision generation wakes.
|
||||||
|
- Preparation mutates no body, clock, FullCell, spatial/shadow registration,
|
||||||
|
bucket, camera, world entity, or presentation resource.
|
||||||
|
|
||||||
|
Legacy direct SetPosition remains a distinct token mode so the dormant slice
|
||||||
|
does not change existing call sites or their warmed allocation ceiling. If a
|
||||||
|
legacy operation becomes deferred and later needs new authored data, the
|
||||||
|
presence of Runtime's preparation authority makes the exact seal mandatory.
|
||||||
|
|
||||||
|
## Ownership and validation
|
||||||
|
|
||||||
|
`RuntimeSetPositionState` owns one exact-key preparation-authority entry only
|
||||||
|
for operations which require authored preparation. The entry dies with the
|
||||||
|
operation on replacement, acknowledgement, cancellation, delete, session
|
||||||
|
reset, or disposal and participates in terminal convergence accounting.
|
||||||
|
|
||||||
|
Preparation-only validation checks the exact cell frame, live-centered world
|
||||||
|
position, values consumed by the first two retail spheres, Setup-derived step
|
||||||
|
heights, line/scatter inputs, and bounded scatter attempts. The legacy direct
|
||||||
|
validator retains its prior behavior, including retail's dummy-sphere and
|
||||||
|
first-two-sphere semantics.
|
||||||
|
|
||||||
|
## Gates and review
|
||||||
|
|
||||||
|
- Focused authored-mover plus SetPosition residence tests: 80/80.
|
||||||
|
- Runtime Release build: zero warnings and zero errors.
|
||||||
|
- Complete Runtime project under invariant culture: 595/595.
|
||||||
|
- Complete Release solution with installed DAT/pak fixtures: 10,342 passed /
|
||||||
|
4 intentional skips.
|
||||||
|
- Retail-conformance review: canonical frame, DID binding, scale/step/sphere
|
||||||
|
behavior, exact SetPosition flags, and deferred wake checked against the
|
||||||
|
named addresses above.
|
||||||
|
- Architecture/adversarial review: command sealing, legacy promotion,
|
||||||
|
replacement, deferred wake, direct compatibility, allocation, reset, GUID
|
||||||
|
reuse, and ownership convergence checked.
|
||||||
|
|
||||||
|
The three ordinary current-culture Runtime failures are pre-existing Swedish-
|
||||||
|
locale formatting assumptions (`0,5` versus `0.5` and localized sky text); the
|
||||||
|
same complete project passes under invariant culture.
|
||||||
|
|
||||||
|
## Next checkpoint
|
||||||
|
|
||||||
|
Implement the dormant atomic local-player physics publication transaction:
|
||||||
|
prepare a private controller/body/clock without canonical mutation, evaluate
|
||||||
|
SetPosition against that candidate, then publish the exact same body relation
|
||||||
|
to `RuntimeEntityRecord` and `RuntimeLocalPlayerMovementState` in one callback-
|
||||||
|
free Runtime commit. App and Headless production activation remains a later
|
||||||
|
checkpoint.
|
||||||
|
|
@ -320,7 +320,7 @@ public sealed class RuntimeEntityDirectory
|
||||||
PhysicsStateFlags state)
|
PhysicsStateFlags state)
|
||||||
{
|
{
|
||||||
EnsureKnown(record);
|
EnsureKnown(record);
|
||||||
record.FinalPhysicsState = state;
|
record.SetFinalPhysicsState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool StopMissileAfterCollision(
|
internal bool StopMissileAfterCollision(
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,14 @@ public sealed class RuntimeEntityRecord
|
||||||
PhysicsBody.State = FinalPhysicsState;
|
PhysicsBody.State = FinalPhysicsState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void SetFinalPhysicsState(PhysicsStateFlags state)
|
||||||
|
{
|
||||||
|
if (state == FinalPhysicsState)
|
||||||
|
return;
|
||||||
|
PhysicsStateMutationVersion++;
|
||||||
|
FinalPhysicsState = state;
|
||||||
|
}
|
||||||
|
|
||||||
/// <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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Net.Messages;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Physics;
|
||||||
|
|
||||||
|
internal enum RuntimeSetPositionMoverPreparationStatus
|
||||||
|
{
|
||||||
|
Prepared,
|
||||||
|
RetrySetupUnavailable,
|
||||||
|
RejectedAuthority,
|
||||||
|
InvalidData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Distinguishes a Setup payload which has not arrived yet from a completed
|
||||||
|
/// lookup whose result is absent. Retail supplies its dummy placement sphere
|
||||||
|
/// only inside <c>CPhysicsObj::SetPosition</c>; an unavailable lookup must not
|
||||||
|
/// manufacture that fallback while asynchronous preparation is still live.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct RuntimeSetPositionMoverSetup(
|
||||||
|
bool IsResolved,
|
||||||
|
uint SetupTableId,
|
||||||
|
FlatSetupCollision? Collision)
|
||||||
|
{
|
||||||
|
internal static RuntimeSetPositionMoverSetup Unavailable => default;
|
||||||
|
|
||||||
|
internal static RuntimeSetPositionMoverSetup ResolvedAbsent =>
|
||||||
|
new(true, 0u, null);
|
||||||
|
|
||||||
|
internal static RuntimeSetPositionMoverSetup Resolved(
|
||||||
|
uint setupTableId,
|
||||||
|
FlatSetupCollision collision) =>
|
||||||
|
new(
|
||||||
|
true,
|
||||||
|
setupTableId != 0u
|
||||||
|
? setupTableId
|
||||||
|
: throw new ArgumentOutOfRangeException(nameof(setupTableId)),
|
||||||
|
collision ?? throw new ArgumentNullException(nameof(collision)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Explicit, immutable inputs surrounding retail's authored mover shape.
|
||||||
|
/// None of these values are inferred from presentation state.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct RuntimeSetPositionMoverPreparation(
|
||||||
|
RuntimeSetPositionMoverSetup Setup,
|
||||||
|
RuntimeSetPositionOperationKind Kind,
|
||||||
|
double GameTime,
|
||||||
|
PhysicsPlacementClass PlacementClass,
|
||||||
|
PhysicsSetPositionFlags Flags,
|
||||||
|
Vector3 Line = default,
|
||||||
|
float ScatterRadiusX = 0f,
|
||||||
|
float ScatterRadiusY = 0f,
|
||||||
|
uint ScatterAttempts = 0u,
|
||||||
|
float ShadowWorldOffsetX = 0f,
|
||||||
|
float ShadowWorldOffsetY = 0f,
|
||||||
|
RuntimePortalPlacementAuthority Portal = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pure preparation port of the mover inputs consumed by retail
|
||||||
|
/// <c>CPhysicsObj::SetPosition</c> (0x005160C0). It preserves the complete DAT
|
||||||
|
/// sphere order; Core's SPHEREPATH port applies retail's two-sphere cap later.
|
||||||
|
/// </summary>
|
||||||
|
internal static class RuntimeSetPositionMoverPreparer
|
||||||
|
{
|
||||||
|
internal static bool TryBuild(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
in CreateObject.ServerPosition acceptedPosition,
|
||||||
|
uint canonicalSetupTableId,
|
||||||
|
RuntimeSetPositionOperationKind acceptedKind,
|
||||||
|
RuntimePortalPlacementAuthority acceptedPortal,
|
||||||
|
ulong velocityAuthorityVersion,
|
||||||
|
in RuntimeSetPositionMoverPreparation preparation,
|
||||||
|
out RuntimeSetPositionCommand command)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
|
command = default;
|
||||||
|
if (!preparation.Setup.IsResolved
|
||||||
|
|| preparation.Kind != acceptedKind
|
||||||
|
|| preparation.Portal != acceptedPortal
|
||||||
|
|| (canonicalSetupTableId == 0u
|
||||||
|
? preparation.Setup.SetupTableId != 0u
|
||||||
|
|| preparation.Setup.Collision is not null
|
||||||
|
: preparation.Setup.SetupTableId != canonicalSetupTableId
|
||||||
|
|| preparation.Setup.Collision is null))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateObject.ServerPosition position = acceptedPosition;
|
||||||
|
Vector3 cellLocal = new(
|
||||||
|
position.PositionX,
|
||||||
|
position.PositionY,
|
||||||
|
position.PositionZ);
|
||||||
|
Vector3 world = new(
|
||||||
|
cellLocal.X + preparation.ShadowWorldOffsetX,
|
||||||
|
cellLocal.Y + preparation.ShadowWorldOffsetY,
|
||||||
|
cellLocal.Z);
|
||||||
|
Quaternion orientation = new(
|
||||||
|
position.RotationX,
|
||||||
|
position.RotationY,
|
||||||
|
position.RotationZ,
|
||||||
|
position.RotationW);
|
||||||
|
|
||||||
|
float scale = record.Snapshot.Physics?.Scale
|
||||||
|
?? record.Snapshot.ObjScale
|
||||||
|
?? 1f;
|
||||||
|
FlatSetupCollision? setup = preparation.Setup.Collision;
|
||||||
|
ImmutableArray<FlatCollisionSphere> spheres = setup?.Spheres
|
||||||
|
?? ImmutableArray<FlatCollisionSphere>.Empty;
|
||||||
|
// CPartArray::GetStepUpHeight/GetStepDownHeight are Setup properties,
|
||||||
|
// not sphere properties. An authored Setup with no collision spheres
|
||||||
|
// still supplies both values; only a genuinely absent Setup takes the
|
||||||
|
// retail dummy path with exact zero steps and no scale multiplication.
|
||||||
|
float stepUp = setup is not null ? setup.StepUpHeight * scale : 0f;
|
||||||
|
float stepDown = setup is not null ? setup.StepDownHeight * scale : 0f;
|
||||||
|
|
||||||
|
EntityCollisionFlags collisionFlags =
|
||||||
|
EntityCollisionFlagsExt.FromPwdBitfield(
|
||||||
|
record.Snapshot.ObjectDescriptionFlags ?? 0u);
|
||||||
|
ObjectInfoState moverFlags = collisionFlags.ToMoverState();
|
||||||
|
if (collisionFlags.HasFlag(EntityCollisionFlags.IsPlayer))
|
||||||
|
moverFlags |= ObjectInfoState.IsPlayer;
|
||||||
|
|
||||||
|
var request = new PhysicsSetPositionRequest(
|
||||||
|
world,
|
||||||
|
orientation,
|
||||||
|
position.LandblockId,
|
||||||
|
cellLocal,
|
||||||
|
spheres,
|
||||||
|
scale,
|
||||||
|
stepUp,
|
||||||
|
stepDown,
|
||||||
|
record.FinalPhysicsState,
|
||||||
|
moverFlags,
|
||||||
|
record.Key?.LocalEntityId ?? 0u,
|
||||||
|
preparation.PlacementClass,
|
||||||
|
preparation.Flags,
|
||||||
|
preparation.Line,
|
||||||
|
preparation.ScatterRadiusX,
|
||||||
|
preparation.ScatterRadiusY,
|
||||||
|
preparation.ScatterAttempts);
|
||||||
|
command = new RuntimeSetPositionCommand(
|
||||||
|
request,
|
||||||
|
preparation.Kind,
|
||||||
|
preparation.GameTime,
|
||||||
|
velocityAuthorityVersion,
|
||||||
|
preparation.ShadowWorldOffsetX,
|
||||||
|
preparation.ShadowWorldOffsetY,
|
||||||
|
preparation.Portal);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
|
|
@ -31,11 +32,18 @@ internal enum RuntimeEntityPlacementStage
|
||||||
CancelledAwaitingAcknowledgement,
|
CancelledAwaitingAcknowledgement,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum RuntimeEntityPlacementPreparationKind : byte
|
||||||
|
{
|
||||||
|
LegacyDirect,
|
||||||
|
AuthoredMover,
|
||||||
|
}
|
||||||
|
|
||||||
internal readonly record struct RuntimeEntityPlacementToken(
|
internal readonly record struct RuntimeEntityPlacementToken(
|
||||||
ulong SessionLifetimeVersion,
|
ulong SessionLifetimeVersion,
|
||||||
RuntimeEntityKey Entity,
|
RuntimeEntityKey Entity,
|
||||||
ulong PositionAuthorityVersion,
|
ulong PositionAuthorityVersion,
|
||||||
ulong OperationId)
|
ulong OperationId,
|
||||||
|
RuntimeEntityPlacementPreparationKind PreparationKind)
|
||||||
{
|
{
|
||||||
internal bool IsValid => OperationId != 0UL
|
internal bool IsValid => OperationId != 0UL
|
||||||
&& Entity.LocalEntityId != 0u
|
&& Entity.LocalEntityId != 0u
|
||||||
|
|
@ -132,14 +140,16 @@ internal readonly record struct RuntimeSetPositionOwnershipSnapshot(
|
||||||
int DeferredBucketOrderCount,
|
int DeferredBucketOrderCount,
|
||||||
int UnboundDeferredCellCount,
|
int UnboundDeferredCellCount,
|
||||||
int UnboundDeferredCellOrderCount,
|
int UnboundDeferredCellOrderCount,
|
||||||
int PreparedMoverCount)
|
int PreparedMoverCount,
|
||||||
|
int MoverPreparationAuthorityCount)
|
||||||
{
|
{
|
||||||
internal bool IndexesConsistent =>
|
internal bool IndexesConsistent =>
|
||||||
LostDeadlineCount == LostDeadlineNodeCount
|
LostDeadlineCount == LostDeadlineNodeCount
|
||||||
&& LostDeadlineCount == LostDeadlineIndexCount
|
&& LostDeadlineCount == LostDeadlineIndexCount
|
||||||
&& ExpiredLostCellCount == ExpiredLostCellIndexCount
|
&& ExpiredLostCellCount == ExpiredLostCellIndexCount
|
||||||
&& DeferredBucketCount == DeferredBucketOrderCount
|
&& DeferredBucketCount == DeferredBucketOrderCount
|
||||||
&& UnboundDeferredCellCount == UnboundDeferredCellOrderCount;
|
&& UnboundDeferredCellCount == UnboundDeferredCellOrderCount
|
||||||
|
&& MoverPreparationAuthorityCount <= ActiveOperationCount;
|
||||||
|
|
||||||
internal bool IsConverged => ActiveOperationCount == 0
|
internal bool IsConverged => ActiveOperationCount == 0
|
||||||
&& AwaitingPreparationCount == 0
|
&& AwaitingPreparationCount == 0
|
||||||
|
|
@ -154,7 +164,8 @@ internal readonly record struct RuntimeSetPositionOwnershipSnapshot(
|
||||||
&& DeferredBucketOrderCount == 0
|
&& DeferredBucketOrderCount == 0
|
||||||
&& UnboundDeferredCellCount == 0
|
&& UnboundDeferredCellCount == 0
|
||||||
&& UnboundDeferredCellOrderCount == 0
|
&& UnboundDeferredCellOrderCount == 0
|
||||||
&& PreparedMoverCount == 0;
|
&& PreparedMoverCount == 0
|
||||||
|
&& MoverPreparationAuthorityCount == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -176,6 +187,20 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
uint CellId,
|
uint CellId,
|
||||||
ulong CollisionGeneration);
|
ulong CollisionGeneration);
|
||||||
|
|
||||||
|
private readonly record struct MoverPreparationAuthority(
|
||||||
|
ulong OperationId,
|
||||||
|
CreateObject.ServerPosition AcceptedPosition,
|
||||||
|
uint SetupTableId,
|
||||||
|
ulong PositionAuthorityVersion,
|
||||||
|
ulong VelocityAuthorityVersion,
|
||||||
|
ulong StateAuthorityVersion,
|
||||||
|
ulong VectorAuthorityVersion,
|
||||||
|
ulong ObjDescAuthorityVersion,
|
||||||
|
ulong CreateIntegrationVersion,
|
||||||
|
ulong PhysicsStateMutationVersion,
|
||||||
|
bool Prepared,
|
||||||
|
RuntimeSetPositionCommand PreparedCommand);
|
||||||
|
|
||||||
private sealed class Operation
|
private sealed class Operation
|
||||||
{
|
{
|
||||||
internal required RuntimeEntityRecord Record { get; init; }
|
internal required RuntimeEntityRecord Record { get; init; }
|
||||||
|
|
@ -249,6 +274,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
_lostDeadlineNodeIndex = [];
|
_lostDeadlineNodeIndex = [];
|
||||||
private readonly Dictionary<RuntimeEntityKey, PhysicsSetPositionRequest>
|
private readonly Dictionary<RuntimeEntityKey, PhysicsSetPositionRequest>
|
||||||
_preparedMovers = [];
|
_preparedMovers = [];
|
||||||
|
private readonly Dictionary<RuntimeEntityKey, MoverPreparationAuthority>
|
||||||
|
_moverPreparationAuthorities = [];
|
||||||
private readonly LinkedList<RuntimeEntityKey> _expiredLostCells = [];
|
private readonly LinkedList<RuntimeEntityKey> _expiredLostCells = [];
|
||||||
private readonly Dictionary<RuntimeEntityKey, LinkedListNode<RuntimeEntityKey>>
|
private readonly Dictionary<RuntimeEntityKey, LinkedListNode<RuntimeEntityKey>>
|
||||||
_expiredLostCellNodes = [];
|
_expiredLostCellNodes = [];
|
||||||
|
|
@ -299,7 +326,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
_deferredBucketOrder.Count,
|
_deferredBucketOrder.Count,
|
||||||
_unboundDeferredByCell.Count,
|
_unboundDeferredByCell.Count,
|
||||||
_unboundDeferredCellOrder.Count,
|
_unboundDeferredCellOrder.Count,
|
||||||
_preparedMovers.Count);
|
_preparedMovers.Count,
|
||||||
|
_moverPreparationAuthorities.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal int PendingProjectionCount => _pendingProjection.Count;
|
internal int PendingProjectionCount => _pendingProjection.Count;
|
||||||
|
|
@ -345,35 +373,71 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
RuntimeEntityPlacementToken token = BeginAcceptedPlacement(
|
RuntimeEntityPlacementToken token = BeginAcceptedPlacementCore(
|
||||||
record,
|
record,
|
||||||
expectedPositionAuthorityVersion,
|
expectedPositionAuthorityVersion,
|
||||||
command.Kind,
|
command.Kind,
|
||||||
command.Portal);
|
command.Portal,
|
||||||
|
captureMoverPreparationAuthority: false);
|
||||||
if (!token.IsValid)
|
if (!token.IsValid)
|
||||||
{
|
{
|
||||||
return Rejected(command.Physics);
|
return Rejected(command.Physics);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SubmitPreparedPlacement(token, command);
|
return SubmitPreparedPlacementCore(
|
||||||
|
token,
|
||||||
|
command,
|
||||||
|
allowDirectUnsealed: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal RuntimeEntityPlacementToken BeginAcceptedPlacement(
|
internal RuntimeEntityPlacementToken BeginAcceptedPlacement(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
ulong expectedPositionAuthorityVersion,
|
ulong expectedPositionAuthorityVersion,
|
||||||
RuntimeSetPositionOperationKind kind,
|
RuntimeSetPositionOperationKind kind,
|
||||||
RuntimePortalPlacementAuthority portal = default)
|
RuntimePortalPlacementAuthority portal = default) =>
|
||||||
|
BeginAcceptedPlacementCore(
|
||||||
|
record,
|
||||||
|
expectedPositionAuthorityVersion,
|
||||||
|
kind,
|
||||||
|
portal,
|
||||||
|
captureMoverPreparationAuthority: false);
|
||||||
|
|
||||||
|
internal RuntimeEntityPlacementToken BeginAuthoredPlacement(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
ulong expectedPositionAuthorityVersion,
|
||||||
|
RuntimeSetPositionOperationKind kind,
|
||||||
|
RuntimePortalPlacementAuthority portal = default) =>
|
||||||
|
BeginAcceptedPlacementCore(
|
||||||
|
record,
|
||||||
|
expectedPositionAuthorityVersion,
|
||||||
|
kind,
|
||||||
|
portal,
|
||||||
|
captureMoverPreparationAuthority: true);
|
||||||
|
|
||||||
|
private RuntimeEntityPlacementToken BeginAcceptedPlacementCore(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
ulong expectedPositionAuthorityVersion,
|
||||||
|
RuntimeSetPositionOperationKind kind,
|
||||||
|
RuntimePortalPlacementAuthority portal,
|
||||||
|
bool captureMoverPreparationAuthority)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
|
CreateObject.ServerPosition? acceptedPosition =
|
||||||
|
record.Snapshot.Physics?.Position ?? record.Snapshot.Position;
|
||||||
if (record.Key is not { } key
|
if (record.Key is not { } key
|
||||||
|| !_entities.IsCurrent(record)
|
|| !_entities.IsCurrent(record)
|
||||||
|| record.PositionAuthorityVersion
|
|| record.PositionAuthorityVersion
|
||||||
!= expectedPositionAuthorityVersion
|
!= expectedPositionAuthorityVersion
|
||||||
|
|| (captureMoverPreparationAuthority
|
||||||
|
&& acceptedPosition is null)
|
||||||
|| !(portal.IsEmpty
|
|| !(portal.IsEmpty
|
||||||
|| (portal.IsValid
|
|| (portal.IsValid
|
||||||
&& kind is RuntimeSetPositionOperationKind
|
&& kind is RuntimeSetPositionOperationKind
|
||||||
.LocalAuthoritative)))
|
.LocalAuthoritative
|
||||||
|
&& acceptedPosition is { } portalPosition
|
||||||
|
&& portal.Projection.DestinationCell
|
||||||
|
== portalPosition.LandblockId)))
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
@ -382,7 +446,10 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
_entities.SessionLifetimeVersion,
|
_entities.SessionLifetimeVersion,
|
||||||
key,
|
key,
|
||||||
expectedPositionAuthorityVersion,
|
expectedPositionAuthorityVersion,
|
||||||
checked(++_nextOperationId));
|
checked(++_nextOperationId),
|
||||||
|
captureMoverPreparationAuthority
|
||||||
|
? RuntimeEntityPlacementPreparationKind.AuthoredMover
|
||||||
|
: RuntimeEntityPlacementPreparationKind.LegacyDirect);
|
||||||
var replacement = new Operation
|
var replacement = new Operation
|
||||||
{
|
{
|
||||||
Record = record,
|
Record = record,
|
||||||
|
|
@ -442,20 +509,95 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
displaced.CollisionGeneration;
|
displaced.CollisionGeneration;
|
||||||
}
|
}
|
||||||
_operations[key] = replacement;
|
_operations[key] = replacement;
|
||||||
|
if (captureMoverPreparationAuthority)
|
||||||
|
{
|
||||||
|
_moverPreparationAuthorities[key] = CapturePreparationAuthority(
|
||||||
|
replacement,
|
||||||
|
acceptedPosition!.Value,
|
||||||
|
prepared: false);
|
||||||
|
}
|
||||||
if (discard is { } cancelled)
|
if (discard is { } cancelled)
|
||||||
PublishPlacement(cancelled);
|
PublishPlacement(cancelled);
|
||||||
return IsCurrent(replacement) ? token : default;
|
return IsCurrent(replacement) ? token : default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal RuntimeSetPositionMoverPreparationStatus PrepareMover(
|
||||||
|
in RuntimeEntityPlacementToken token,
|
||||||
|
in RuntimeSetPositionMoverPreparation preparation,
|
||||||
|
out RuntimeSetPositionCommand command)
|
||||||
|
{
|
||||||
|
EnsureNotDisposed();
|
||||||
|
command = default;
|
||||||
|
if (!token.IsValid
|
||||||
|
|| !_operations.TryGetValue(token.Entity, out Operation? operation)
|
||||||
|
|| operation.Token != token
|
||||||
|
|| operation.Stage
|
||||||
|
is not RuntimeEntityPlacementStage.AwaitingPreparation
|
||||||
|
|| !IsCurrent(operation)
|
||||||
|
|| !_moverPreparationAuthorities.TryGetValue(
|
||||||
|
token.Entity,
|
||||||
|
out MoverPreparationAuthority authority)
|
||||||
|
|| authority.OperationId != token.OperationId
|
||||||
|
|| !IsPreparationAuthorityCurrent(operation, authority))
|
||||||
|
{
|
||||||
|
return RuntimeSetPositionMoverPreparationStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preparation.Setup.IsResolved)
|
||||||
|
{
|
||||||
|
return RuntimeSetPositionMoverPreparationStatus
|
||||||
|
.RetrySetupUnavailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RuntimeSetPositionMoverPreparer.TryBuild(
|
||||||
|
operation.Record,
|
||||||
|
authority.AcceptedPosition,
|
||||||
|
authority.SetupTableId,
|
||||||
|
operation.Kind,
|
||||||
|
operation.Portal,
|
||||||
|
authority.VelocityAuthorityVersion,
|
||||||
|
preparation,
|
||||||
|
out command)
|
||||||
|
|| !IsStructurallyValid(command.Physics))
|
||||||
|
{
|
||||||
|
command = default;
|
||||||
|
return RuntimeSetPositionMoverPreparationStatus.InvalidData;
|
||||||
|
}
|
||||||
|
|
||||||
|
_moverPreparationAuthorities[token.Entity] = authority with
|
||||||
|
{
|
||||||
|
Prepared = true,
|
||||||
|
PreparedCommand = command,
|
||||||
|
};
|
||||||
|
|
||||||
|
return RuntimeSetPositionMoverPreparationStatus.Prepared;
|
||||||
|
}
|
||||||
|
|
||||||
internal RuntimeSetPositionOutcome SubmitPreparedPlacement(
|
internal RuntimeSetPositionOutcome SubmitPreparedPlacement(
|
||||||
in RuntimeEntityPlacementToken token,
|
in RuntimeEntityPlacementToken token,
|
||||||
in RuntimeSetPositionCommand command)
|
in RuntimeSetPositionCommand command) =>
|
||||||
|
SubmitPreparedPlacementCore(
|
||||||
|
token,
|
||||||
|
command,
|
||||||
|
allowDirectUnsealed: token.PreparationKind
|
||||||
|
is RuntimeEntityPlacementPreparationKind.LegacyDirect);
|
||||||
|
|
||||||
|
private RuntimeSetPositionOutcome SubmitPreparedPlacementCore(
|
||||||
|
in RuntimeEntityPlacementToken token,
|
||||||
|
in RuntimeSetPositionCommand command,
|
||||||
|
bool allowDirectUnsealed)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
Operation? operation = null;
|
Operation? operation = null;
|
||||||
bool ownsToken = token.IsValid
|
bool ownsToken = token.IsValid
|
||||||
&& _operations.TryGetValue(token.Entity, out operation)
|
&& _operations.TryGetValue(token.Entity, out operation)
|
||||||
&& operation.Token == token;
|
&& operation.Token == token;
|
||||||
|
MoverPreparationAuthority exactAuthority = default;
|
||||||
|
bool hasPreparationAuthority = token.IsValid
|
||||||
|
&& _moverPreparationAuthorities.TryGetValue(
|
||||||
|
token.Entity,
|
||||||
|
out exactAuthority)
|
||||||
|
&& exactAuthority.OperationId == token.OperationId;
|
||||||
if (!ownsToken
|
if (!ownsToken
|
||||||
|| operation is null
|
|| operation is null
|
||||||
|| operation.Stage
|
|| operation.Stage
|
||||||
|
|
@ -465,6 +607,13 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
|| !double.IsFinite(command.GameTime)
|
|| !double.IsFinite(command.GameTime)
|
||||||
|| command.Kind != operation.Kind
|
|| command.Kind != operation.Kind
|
||||||
|| command.Portal != operation.Portal
|
|| command.Portal != operation.Portal
|
||||||
|
|| (hasPreparationAuthority
|
||||||
|
? !exactAuthority.Prepared
|
||||||
|
|| exactAuthority.PreparedCommand != command
|
||||||
|
|| !IsPreparationAuthorityCurrent(
|
||||||
|
operation,
|
||||||
|
exactAuthority)
|
||||||
|
: !allowDirectUnsealed)
|
||||||
|| (command.ExpectedVelocityAuthorityVersion != 0UL
|
|| (command.ExpectedVelocityAuthorityVersion != 0UL
|
||||||
&& operation.Record.VelocityAuthorityVersion
|
&& operation.Record.VelocityAuthorityVersion
|
||||||
!= command.ExpectedVelocityAuthorityVersion))
|
!= command.ExpectedVelocityAuthorityVersion))
|
||||||
|
|
@ -511,6 +660,13 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
};
|
};
|
||||||
var canonicalCommand = command with { Physics = canonicalRequest };
|
var canonicalCommand = command with { Physics = canonicalRequest };
|
||||||
operation.Command = canonicalCommand;
|
operation.Command = canonicalCommand;
|
||||||
|
if (hasPreparationAuthority)
|
||||||
|
{
|
||||||
|
_moverPreparationAuthorities[token.Entity] = exactAuthority with
|
||||||
|
{
|
||||||
|
PreparedCommand = canonicalCommand,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (operation.InheritedLostDeadline
|
if (operation.InheritedLostDeadline
|
||||||
&& !operation.WithdrawalAcknowledged
|
&& !operation.WithdrawalAcknowledged
|
||||||
|
|
@ -657,11 +813,13 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
if (pending.Kind is RuntimePlacementProjectionKind.Place)
|
if (pending.Kind is RuntimePlacementProjectionKind.Place)
|
||||||
{
|
{
|
||||||
operation.Stage = RuntimeEntityPlacementStage.AwaitingCommitAcknowledgement;
|
operation.Stage = RuntimeEntityPlacementStage.AwaitingCommitAcknowledgement;
|
||||||
|
_moverPreparationAuthorities.Remove(operation.Key);
|
||||||
return _operations.Remove(operation.Key);
|
return _operations.Remove(operation.Key);
|
||||||
}
|
}
|
||||||
if (!operation.WakeableLostCell
|
if (!operation.WakeableLostCell
|
||||||
&& !operation.InheritedLostDeadline)
|
&& !operation.InheritedLostDeadline)
|
||||||
{
|
{
|
||||||
|
_moverPreparationAuthorities.Remove(operation.Key);
|
||||||
return _operations.Remove(operation.Key);
|
return _operations.Remove(operation.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -934,7 +1092,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
_entities.SessionLifetimeVersion,
|
_entities.SessionLifetimeVersion,
|
||||||
key,
|
key,
|
||||||
record.PositionAuthorityVersion,
|
record.PositionAuthorityVersion,
|
||||||
checked(++_nextOperationId)),
|
checked(++_nextOperationId),
|
||||||
|
RuntimeEntityPlacementPreparationKind.AuthoredMover),
|
||||||
Key = key,
|
Key = key,
|
||||||
PositionAuthorityVersion = record.PositionAuthorityVersion,
|
PositionAuthorityVersion = record.PositionAuthorityVersion,
|
||||||
SessionLifetimeVersion = _entities.SessionLifetimeVersion,
|
SessionLifetimeVersion = _entities.SessionLifetimeVersion,
|
||||||
|
|
@ -953,6 +1112,14 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
RequiresPreparation = !hasPrepared,
|
RequiresPreparation = !hasPrepared,
|
||||||
};
|
};
|
||||||
_operations.Add(key, operation);
|
_operations.Add(key, operation);
|
||||||
|
_moverPreparationAuthorities[key] = CapturePreparationAuthority(
|
||||||
|
operation,
|
||||||
|
ServerPositionFrom(
|
||||||
|
cellId,
|
||||||
|
body.CellPosition.Frame.Origin,
|
||||||
|
body.Orientation),
|
||||||
|
prepared: hasPrepared,
|
||||||
|
command);
|
||||||
RuntimeSetPositionOutcome parked = ParkDeferred(
|
RuntimeSetPositionOutcome parked = ParkDeferred(
|
||||||
operation,
|
operation,
|
||||||
result,
|
result,
|
||||||
|
|
@ -1159,6 +1326,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
_lostDeadlineNodes.Clear();
|
_lostDeadlineNodes.Clear();
|
||||||
_lostDeadlineNodeIndex.Clear();
|
_lostDeadlineNodeIndex.Clear();
|
||||||
_preparedMovers.Clear();
|
_preparedMovers.Clear();
|
||||||
|
_moverPreparationAuthorities.Clear();
|
||||||
_pendingProjection.Clear();
|
_pendingProjection.Clear();
|
||||||
_expiredLostCells.Clear();
|
_expiredLostCells.Clear();
|
||||||
_expiredLostCellNodes.Clear();
|
_expiredLostCellNodes.Clear();
|
||||||
|
|
@ -1215,6 +1383,22 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
CurrentCellId = null,
|
CurrentCellId = null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
if (_moverPreparationAuthorities.ContainsKey(operation.Key))
|
||||||
|
{
|
||||||
|
RebindPreparedCommand(operation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_moverPreparationAuthorities[operation.Key] =
|
||||||
|
CapturePreparationAuthority(
|
||||||
|
operation,
|
||||||
|
ServerPositionFrom(
|
||||||
|
result.CellId,
|
||||||
|
result.CellLocalPosition,
|
||||||
|
result.Orientation),
|
||||||
|
prepared: true,
|
||||||
|
operation.Command);
|
||||||
|
}
|
||||||
IndexDeferred(operation);
|
IndexDeferred(operation);
|
||||||
operation.Stage = operation.RequiresPreparation
|
operation.Stage = operation.RequiresPreparation
|
||||||
? RuntimeEntityPlacementStage.AwaitingPreparation
|
? RuntimeEntityPlacementStage.AwaitingPreparation
|
||||||
|
|
@ -1243,12 +1427,16 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!IsDeferredWakePreparationCurrent(operation))
|
||||||
|
return;
|
||||||
|
|
||||||
UnindexDeferred(operation);
|
UnindexDeferred(operation);
|
||||||
operation.Command = operation.Command with
|
operation.Command = operation.Command with
|
||||||
{
|
{
|
||||||
GameTime = _physics.PlacementSimulationTime(
|
GameTime = _physics.PlacementSimulationTime(
|
||||||
operation.Command.GameTime),
|
operation.Command.GameTime),
|
||||||
};
|
};
|
||||||
|
RebindPreparedCommand(operation);
|
||||||
PhysicsSetPositionResult result = IsStructurallyValid(
|
PhysicsSetPositionResult result = IsStructurallyValid(
|
||||||
operation.Command.Physics)
|
operation.Command.Physics)
|
||||||
? _physics.Engine.SetPosition(
|
? _physics.Engine.SetPosition(
|
||||||
|
|
@ -1626,7 +1814,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
_entities.SessionLifetimeVersion,
|
_entities.SessionLifetimeVersion,
|
||||||
key,
|
key,
|
||||||
record.PositionAuthorityVersion,
|
record.PositionAuthorityVersion,
|
||||||
checked(++_nextOperationId)),
|
checked(++_nextOperationId),
|
||||||
|
RuntimeEntityPlacementPreparationKind.LegacyDirect),
|
||||||
Key = key,
|
Key = key,
|
||||||
PositionAuthorityVersion = record.PositionAuthorityVersion,
|
PositionAuthorityVersion = record.PositionAuthorityVersion,
|
||||||
SessionLifetimeVersion = _entities.SessionLifetimeVersion,
|
SessionLifetimeVersion = _entities.SessionLifetimeVersion,
|
||||||
|
|
@ -1673,6 +1862,104 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
|| operation.Record.VelocityAuthorityVersion
|
|| operation.Record.VelocityAuthorityVersion
|
||||||
== operation.SourceVelocityAuthorityVersion;
|
== operation.SourceVelocityAuthorityVersion;
|
||||||
|
|
||||||
|
private static MoverPreparationAuthority CapturePreparationAuthority(
|
||||||
|
Operation operation,
|
||||||
|
in CreateObject.ServerPosition acceptedPosition,
|
||||||
|
bool prepared,
|
||||||
|
in RuntimeSetPositionCommand preparedCommand = default) => new(
|
||||||
|
operation.Token.OperationId,
|
||||||
|
acceptedPosition,
|
||||||
|
CanonicalSetupTableId(operation.Record),
|
||||||
|
operation.Record.PositionAuthorityVersion,
|
||||||
|
operation.Record.VelocityAuthorityVersion,
|
||||||
|
operation.Record.StateAuthorityVersion,
|
||||||
|
operation.Record.VectorAuthorityVersion,
|
||||||
|
operation.Record.ObjDescAuthorityVersion,
|
||||||
|
operation.Record.CreateIntegrationVersion,
|
||||||
|
operation.Record.PhysicsStateMutationVersion,
|
||||||
|
prepared,
|
||||||
|
preparedCommand);
|
||||||
|
|
||||||
|
private static uint CanonicalSetupTableId(RuntimeEntityRecord record) =>
|
||||||
|
record.Snapshot.Physics?.SetupTableId
|
||||||
|
?? record.Snapshot.SetupTableId
|
||||||
|
?? 0u;
|
||||||
|
|
||||||
|
private static CreateObject.ServerPosition ServerPositionFrom(
|
||||||
|
uint cellId,
|
||||||
|
Vector3 cellLocal,
|
||||||
|
Quaternion orientation) => new(
|
||||||
|
cellId,
|
||||||
|
cellLocal.X,
|
||||||
|
cellLocal.Y,
|
||||||
|
cellLocal.Z,
|
||||||
|
orientation.W,
|
||||||
|
orientation.X,
|
||||||
|
orientation.Y,
|
||||||
|
orientation.Z);
|
||||||
|
|
||||||
|
private static bool IsPreparationAuthorityCurrent(
|
||||||
|
Operation operation,
|
||||||
|
in MoverPreparationAuthority authority) =>
|
||||||
|
operation.Record.PositionAuthorityVersion
|
||||||
|
== authority.PositionAuthorityVersion
|
||||||
|
&& operation.Record.VelocityAuthorityVersion
|
||||||
|
== authority.VelocityAuthorityVersion
|
||||||
|
&& operation.Record.StateAuthorityVersion
|
||||||
|
== authority.StateAuthorityVersion
|
||||||
|
&& operation.Record.VectorAuthorityVersion
|
||||||
|
== authority.VectorAuthorityVersion
|
||||||
|
&& operation.Record.ObjDescAuthorityVersion
|
||||||
|
== authority.ObjDescAuthorityVersion
|
||||||
|
&& operation.Record.CreateIntegrationVersion
|
||||||
|
== authority.CreateIntegrationVersion
|
||||||
|
&& operation.Record.PhysicsStateMutationVersion
|
||||||
|
== authority.PhysicsStateMutationVersion;
|
||||||
|
|
||||||
|
private void RebindPreparedCommand(Operation operation)
|
||||||
|
{
|
||||||
|
if (_moverPreparationAuthorities.TryGetValue(
|
||||||
|
operation.Key,
|
||||||
|
out MoverPreparationAuthority authority)
|
||||||
|
&& authority.OperationId == operation.Token.OperationId
|
||||||
|
&& authority.Prepared)
|
||||||
|
{
|
||||||
|
_moverPreparationAuthorities[operation.Key] = authority with
|
||||||
|
{
|
||||||
|
PreparedCommand = operation.Command,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDeferredWakePreparationCurrent(Operation operation)
|
||||||
|
{
|
||||||
|
if (_moverPreparationAuthorities.TryGetValue(
|
||||||
|
operation.Key,
|
||||||
|
out MoverPreparationAuthority authority)
|
||||||
|
&& authority.OperationId == operation.Token.OperationId
|
||||||
|
&& authority.Prepared
|
||||||
|
&& authority.PreparedCommand == operation.Command
|
||||||
|
&& IsPreparationAuthorityCurrent(operation, authority))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
operation.SourceVelocityAuthorityVersion =
|
||||||
|
operation.Record.VelocityAuthorityVersion;
|
||||||
|
operation.RequiresPreparation = true;
|
||||||
|
operation.Stage = RuntimeEntityPlacementStage.AwaitingPreparation;
|
||||||
|
operation.PreparedCommandAwaitingWithdrawalAck = null;
|
||||||
|
_moverPreparationAuthorities[operation.Key] =
|
||||||
|
CapturePreparationAuthority(
|
||||||
|
operation,
|
||||||
|
ServerPositionFrom(
|
||||||
|
operation.ExactCellId,
|
||||||
|
operation.Result.CellLocalPosition,
|
||||||
|
operation.Result.Orientation),
|
||||||
|
prepared: false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
internal void PublishCancellation(
|
internal void PublishCancellation(
|
||||||
in RuntimePlacementCancellationReceipt receipt)
|
in RuntimePlacementCancellationReceipt receipt)
|
||||||
{
|
{
|
||||||
|
|
@ -1712,6 +1999,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
CancelExactLostKey(key);
|
CancelExactLostKey(key);
|
||||||
if (!_operations.Remove(key, out Operation? operation))
|
if (!_operations.Remove(key, out Operation? operation))
|
||||||
return false;
|
return false;
|
||||||
|
_moverPreparationAuthorities.Remove(key);
|
||||||
UnindexDeferred(operation);
|
UnindexDeferred(operation);
|
||||||
if (!preserveLostFamily && cancelLostFamily)
|
if (!preserveLostFamily && cancelLostFamily)
|
||||||
CancelLostFamilyDeadlines(operation);
|
CancelLostFamilyDeadlines(operation);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,715 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Net;
|
||||||
|
using AcDream.Core.Net.Messages;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Physics;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Tests.Physics;
|
||||||
|
|
||||||
|
public sealed class RuntimeSetPositionMoverPreparationTests
|
||||||
|
{
|
||||||
|
private const uint Cell = 0xA9B40021u;
|
||||||
|
private const uint SetupId = 0x02000001u;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedMoverPreservesCompleteAuthoredRetailInput()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
float half = MathF.Sqrt(0.5f);
|
||||||
|
var acceptedPosition = new CreateObject.ServerPosition(
|
||||||
|
Cell,
|
||||||
|
12.5f,
|
||||||
|
23.5f,
|
||||||
|
34.5f,
|
||||||
|
half,
|
||||||
|
0f,
|
||||||
|
half,
|
||||||
|
0f);
|
||||||
|
RuntimeEntityRecord record = CreateRecord(
|
||||||
|
lifetime,
|
||||||
|
objScale: 3f,
|
||||||
|
physicsScale: 2f,
|
||||||
|
objectDescriptionFlags: 0x8u | 0x20u | 0x200000u
|
||||||
|
| 0x2000000u | 0x100000u | 0x400000u,
|
||||||
|
position: acceptedPosition);
|
||||||
|
const PhysicsStateFlags state = PhysicsStateFlags.Gravity
|
||||||
|
| PhysicsStateFlags.EdgeSlide
|
||||||
|
| PhysicsStateFlags.PathClipped;
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(record, state);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
ImmutableArray<FlatCollisionSphere> spheres =
|
||||||
|
[
|
||||||
|
new(new Vector3(1f, 2f, 3f), 0.1f),
|
||||||
|
new(new Vector3(4f, 5f, 6f), 0.2f),
|
||||||
|
new(new Vector3(7f, 8f, 9f), 0.3f),
|
||||||
|
];
|
||||||
|
FlatSetupCollision setup = Setup(spheres, 0.25f, -0.5f);
|
||||||
|
var portal = default(RuntimePortalPlacementAuthority);
|
||||||
|
var input = new RuntimeSetPositionMoverPreparation(
|
||||||
|
RuntimeSetPositionMoverSetup.Resolved(SetupId, setup),
|
||||||
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
||||||
|
GameTime: 42.25d,
|
||||||
|
PhysicsPlacementClass.Corpse,
|
||||||
|
PhysicsSetPositionFlags.Line | PhysicsSetPositionFlags.Scatter,
|
||||||
|
Line: new Vector3(3f, 4f, 5f),
|
||||||
|
ScatterRadiusX: 6f,
|
||||||
|
ScatterRadiusY: 7f,
|
||||||
|
ScatterAttempts: 8u,
|
||||||
|
ShadowWorldOffsetX: 9f,
|
||||||
|
ShadowWorldOffsetY: 10f,
|
||||||
|
Portal: portal);
|
||||||
|
|
||||||
|
RuntimeSetPositionMoverPreparationStatus status = lifetime.Physics
|
||||||
|
.SetPosition.PrepareMover(token, input, out var command);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared, status);
|
||||||
|
Assert.Equal(spheres, command.Physics.Spheres);
|
||||||
|
Assert.Equal(2f, command.Physics.Scale);
|
||||||
|
Assert.Equal(0.5f, command.Physics.StepUpHeight);
|
||||||
|
Assert.Equal(-1f, command.Physics.StepDownHeight);
|
||||||
|
Assert.Equal(Cell, command.Physics.CellId);
|
||||||
|
Assert.Equal(new Vector3(12.5f, 23.5f, 34.5f),
|
||||||
|
command.Physics.CellLocalPosition);
|
||||||
|
Assert.Equal(new Vector3(
|
||||||
|
12.5f + 9f,
|
||||||
|
23.5f + 10f,
|
||||||
|
34.5f), command.Physics.Position);
|
||||||
|
Assert.Equal(new Quaternion(0f, half, 0f, half),
|
||||||
|
command.Physics.Orientation);
|
||||||
|
Assert.Equal(state, command.Physics.MoverPhysicsState);
|
||||||
|
Assert.Equal(
|
||||||
|
ObjectInfoState.IsPlayer
|
||||||
|
| ObjectInfoState.IsPK
|
||||||
|
| ObjectInfoState.IsPKLite
|
||||||
|
| ObjectInfoState.IsImpenetrable
|
||||||
|
| ObjectInfoState.CanBypassMoveRestrictions,
|
||||||
|
command.Physics.MoverFlags);
|
||||||
|
Assert.False(command.Physics.MoverFlags.HasFlag(
|
||||||
|
ObjectInfoState.EdgeSlide));
|
||||||
|
Assert.False(command.Physics.MoverFlags.HasFlag(
|
||||||
|
ObjectInfoState.PathClipped));
|
||||||
|
Assert.False(command.Physics.MoverFlags.HasFlag(
|
||||||
|
ObjectInfoState.FreeRotate));
|
||||||
|
Assert.False(command.Physics.MoverFlags.HasFlag(
|
||||||
|
ObjectInfoState.Contact));
|
||||||
|
Assert.False(command.Physics.MoverFlags.HasFlag(
|
||||||
|
ObjectInfoState.OnWalkable));
|
||||||
|
Assert.Equal(record.Key!.Value.LocalEntityId,
|
||||||
|
command.Physics.MovingEntityId);
|
||||||
|
Assert.Equal(PhysicsPlacementClass.Corpse,
|
||||||
|
command.Physics.PlacementClass);
|
||||||
|
Assert.Equal(input.Flags, command.Physics.Flags);
|
||||||
|
Assert.Equal(input.Line, command.Physics.Line);
|
||||||
|
Assert.Equal(6f, command.Physics.ScatterRadiusX);
|
||||||
|
Assert.Equal(7f, command.Physics.ScatterRadiusY);
|
||||||
|
Assert.Equal(8u, command.Physics.ScatterAttempts);
|
||||||
|
Assert.Equal(42.25d, command.GameTime);
|
||||||
|
Assert.Equal(9f, command.ShadowWorldOffsetX);
|
||||||
|
Assert.Equal(10f, command.ShadowWorldOffsetY);
|
||||||
|
Assert.Equal(portal, command.Portal);
|
||||||
|
Assert.Equal(record.VelocityAuthorityVersion,
|
||||||
|
command.ExpectedVelocityAuthorityVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LocalPortalKindAndAuthorityArePreservedWithoutInference()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime);
|
||||||
|
var portal = new RuntimePortalPlacementAuthority(
|
||||||
|
Present: true,
|
||||||
|
RevealGeneration: 17,
|
||||||
|
TeleportSequence: 3,
|
||||||
|
new RuntimeWorldHostProjectionToken(17, Cell));
|
||||||
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
||||||
|
.BeginAuthoredPlacement(
|
||||||
|
record,
|
||||||
|
record.PositionAuthorityVersion,
|
||||||
|
RuntimeSetPositionOperationKind.LocalAuthoritative,
|
||||||
|
portal);
|
||||||
|
var input = Input(ResolvedEmptySetup()) with
|
||||||
|
{
|
||||||
|
Kind = RuntimeSetPositionOperationKind.LocalAuthoritative,
|
||||||
|
Portal = portal,
|
||||||
|
};
|
||||||
|
|
||||||
|
RuntimeSetPositionMoverPreparationStatus status = lifetime.Physics
|
||||||
|
.SetPosition.PrepareMover(token, input, out var command);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared, status);
|
||||||
|
Assert.Equal(RuntimeSetPositionOperationKind.LocalAuthoritative,
|
||||||
|
command.Kind);
|
||||||
|
Assert.Equal(portal, command.Portal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnavailableSetupRetriesWithoutInventingDummyWhileAbsentAndEmptyResolve()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime, setupTableId: null);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
|
||||||
|
RuntimeSetPositionMoverPreparationStatus unavailable = lifetime.Physics
|
||||||
|
.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.Unavailable),
|
||||||
|
out RuntimeSetPositionCommand unavailableCommand);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable,
|
||||||
|
unavailable);
|
||||||
|
Assert.Equal(default, unavailableCommand);
|
||||||
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
||||||
|
.AwaitingSetPositionPreparationCount);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.ResolvedAbsent),
|
||||||
|
out RuntimeSetPositionCommand absent));
|
||||||
|
Assert.Empty(absent.Physics.Spheres);
|
||||||
|
|
||||||
|
record = CreateRecord(lifetime, guid: 0x70002002u);
|
||||||
|
token = Begin(lifetime, record);
|
||||||
|
FlatSetupCollision authoredEmpty = Setup(
|
||||||
|
ImmutableArray<FlatCollisionSphere>.Empty,
|
||||||
|
0f,
|
||||||
|
0f);
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.Resolved(
|
||||||
|
SetupId,
|
||||||
|
authoredEmpty)),
|
||||||
|
out RuntimeSetPositionCommand empty));
|
||||||
|
Assert.Empty(empty.Physics.Spheres);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KnownSetupMustResolveTheExactCanonicalDid()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.Unavailable),
|
||||||
|
out _));
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeSetPositionMoverPreparationStatus.InvalidData,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.ResolvedAbsent),
|
||||||
|
out _));
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeSetPositionMoverPreparationStatus.InvalidData,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.Resolved(
|
||||||
|
SetupId + 1u,
|
||||||
|
Setup(ImmutableArray<FlatCollisionSphere>.Empty, 0f, 0f))),
|
||||||
|
out _));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(ResolvedEmptySetup()),
|
||||||
|
out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AbsentSetupRejectsInventedCollisionPayload()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(
|
||||||
|
lifetime,
|
||||||
|
setupTableId: null);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeSetPositionMoverPreparationStatus.InvalidData,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.Resolved(
|
||||||
|
SetupId,
|
||||||
|
Setup(ImmutableArray<FlatCollisionSphere>.Empty, 0f, 0f))),
|
||||||
|
out _));
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.ResolvedAbsent),
|
||||||
|
out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthoredEmptySetupStillSuppliesScaledStepHeights()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(
|
||||||
|
lifetime,
|
||||||
|
objScale: 2f);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
FlatSetupCollision authoredEmpty = Setup(
|
||||||
|
ImmutableArray<FlatCollisionSphere>.Empty,
|
||||||
|
0.25f,
|
||||||
|
-0.5f);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.Resolved(
|
||||||
|
SetupId,
|
||||||
|
authoredEmpty)),
|
||||||
|
out RuntimeSetPositionCommand command));
|
||||||
|
Assert.Empty(command.Physics.Spheres);
|
||||||
|
Assert.Equal(0.5f, command.Physics.StepUpHeight);
|
||||||
|
Assert.Equal(-1f, command.Physics.StepDownHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthoredTokenRejectsManualUnsealedSubmission()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime);
|
||||||
|
AttachBody(lifetime, record);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
RuntimeSetPositionCommand invented = new(
|
||||||
|
new PhysicsSetPositionRequest(
|
||||||
|
new Vector3(1f, 2f, 3f),
|
||||||
|
Quaternion.Identity,
|
||||||
|
Cell,
|
||||||
|
new Vector3(1f, 2f, 3f),
|
||||||
|
ImmutableArray<FlatCollisionSphere>.Empty,
|
||||||
|
1f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
record.FinalPhysicsState,
|
||||||
|
ObjectInfoState.None,
|
||||||
|
record.Key!.Value.LocalEntityId),
|
||||||
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
||||||
|
1d,
|
||||||
|
record.VelocityAuthorityVersion);
|
||||||
|
|
||||||
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition
|
||||||
|
.SubmitPreparedPlacement(token, invented);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionStatus.Rejected, outcome.Status);
|
||||||
|
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||||
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
||||||
|
.AwaitingSetPositionPreparationCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RepreparationReplacesTheExactSealedCommand()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime);
|
||||||
|
AttachBody(lifetime, record);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(ResolvedEmptySetup()) with { GameTime = 1d },
|
||||||
|
out RuntimeSetPositionCommand first));
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(ResolvedEmptySetup()) with { GameTime = 2d },
|
||||||
|
out RuntimeSetPositionCommand second));
|
||||||
|
Assert.NotEqual(first, second);
|
||||||
|
|
||||||
|
RuntimeSetPositionOutcome stale = lifetime.Physics.SetPosition
|
||||||
|
.SubmitPreparedPlacement(token, first);
|
||||||
|
Assert.Equal(RuntimeSetPositionStatus.Rejected, stale.Status);
|
||||||
|
|
||||||
|
RuntimeSetPositionOutcome current = lifetime.Physics.SetPosition
|
||||||
|
.SubmitPreparedPlacement(token, second);
|
||||||
|
Assert.NotEqual(RuntimeSetPositionStatus.Rejected, current.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, null, 1f)]
|
||||||
|
[InlineData(0f, null, 0f)]
|
||||||
|
[InlineData(-2f, null, -2f)]
|
||||||
|
[InlineData(3f, 0f, 0f)]
|
||||||
|
[InlineData(3f, -4f, -4f)]
|
||||||
|
public void ScalePreservesAbsentZeroNegativeAndPhysicsPrecedence(
|
||||||
|
float? objScale,
|
||||||
|
float? physicsScale,
|
||||||
|
float expected)
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(
|
||||||
|
lifetime,
|
||||||
|
objScale,
|
||||||
|
physicsScale);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
|
||||||
|
RuntimeSetPositionMoverPreparationStatus status = lifetime.Physics
|
||||||
|
.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.Resolved(SetupId, Setup(
|
||||||
|
[new FlatCollisionSphere(Vector3.Zero, 0.5f)],
|
||||||
|
0.25f,
|
||||||
|
-0.5f))),
|
||||||
|
out RuntimeSetPositionCommand command);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared, status);
|
||||||
|
Assert.Equal(expected, command.Physics.Scale);
|
||||||
|
Assert.Equal(0.25f * expected, command.Physics.StepUpHeight);
|
||||||
|
Assert.Equal(-0.5f * expected, command.Physics.StepDownHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(float.NaN)]
|
||||||
|
[InlineData(float.PositiveInfinity)]
|
||||||
|
[InlineData(float.NegativeInfinity)]
|
||||||
|
public void NonFiniteScaleRejectsWhenAuthoredSphereConsumesIt(float scale)
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime, scale, null);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
|
||||||
|
RuntimeSetPositionMoverPreparationStatus status = lifetime.Physics
|
||||||
|
.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.Resolved(
|
||||||
|
SetupId,
|
||||||
|
Setup(
|
||||||
|
[new FlatCollisionSphere(Vector3.Zero, 0.5f)],
|
||||||
|
0f,
|
||||||
|
0f))),
|
||||||
|
out RuntimeSetPositionCommand command);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.InvalidData, status);
|
||||||
|
Assert.Equal(default, command);
|
||||||
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
||||||
|
.AwaitingSetPositionPreparationCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(float.NaN)]
|
||||||
|
[InlineData(float.PositiveInfinity)]
|
||||||
|
[InlineData(float.NegativeInfinity)]
|
||||||
|
public void NonFiniteScaleIsNotConsumedByResolvedAbsentDummy(float scale)
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(
|
||||||
|
lifetime,
|
||||||
|
objScale: scale,
|
||||||
|
setupTableId: null);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
|
||||||
|
RuntimeSetPositionMoverPreparationStatus status = lifetime.Physics
|
||||||
|
.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.ResolvedAbsent),
|
||||||
|
out RuntimeSetPositionCommand command);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared, status);
|
||||||
|
Assert.Equal(scale, command.Physics.Scale);
|
||||||
|
Assert.Equal(0f, command.Physics.StepUpHeight);
|
||||||
|
Assert.Equal(0f, command.Physics.StepDownHeight);
|
||||||
|
Assert.Empty(command.Physics.Spheres);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparationDoesNotMutateCanonicalOrPresentationState()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime);
|
||||||
|
var body = new PhysicsBody
|
||||||
|
{
|
||||||
|
Position = new Vector3(1f, 2f, 3f),
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
State = PhysicsStateFlags.Gravity,
|
||||||
|
InWorld = true,
|
||||||
|
};
|
||||||
|
body.SnapToCell(0xA8B40001u, body.Position, body.Position);
|
||||||
|
lifetime.Entities.SetPhysicsBody(record, body);
|
||||||
|
lifetime.Entities.SetFullCell(record, 0xA8B40001u, 0xA8B4FFFFu);
|
||||||
|
Vector3 priorPosition = body.Position;
|
||||||
|
uint priorCell = record.FullCellId;
|
||||||
|
ulong priorSpatial = record.SpatialAuthorityVersion;
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(ResolvedEmptySetup()),
|
||||||
|
out _));
|
||||||
|
|
||||||
|
Assert.Same(body, record.PhysicsBody);
|
||||||
|
Assert.Equal(priorPosition, body.Position);
|
||||||
|
Assert.Equal(priorCell, record.FullCellId);
|
||||||
|
Assert.Equal(priorSpatial, record.SpatialAuthorityVersion);
|
||||||
|
Assert.True(body.InWorld);
|
||||||
|
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||||
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
||||||
|
.AwaitingSetPositionPreparationCount);
|
||||||
|
Assert.False(lifetime.Physics.SetPosition
|
||||||
|
.TryGetPreparedMoverSphereCount(record, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(AuthorityReplacement.Position)]
|
||||||
|
[InlineData(AuthorityReplacement.Velocity)]
|
||||||
|
[InlineData(AuthorityReplacement.Vector)]
|
||||||
|
[InlineData(AuthorityReplacement.State)]
|
||||||
|
[InlineData(AuthorityReplacement.ObjectDescription)]
|
||||||
|
[InlineData(AuthorityReplacement.Create)]
|
||||||
|
public void AnyAuthorityReplacementRejectsPreparedSubmissionWithoutMutation(
|
||||||
|
AuthorityReplacement replacement)
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime);
|
||||||
|
var body = new PhysicsBody
|
||||||
|
{
|
||||||
|
Position = new Vector3(1f, 2f, 3f),
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
State = record.FinalPhysicsState,
|
||||||
|
InWorld = true,
|
||||||
|
};
|
||||||
|
body.SnapToCell(0xA8B40001u, body.Position, body.Position);
|
||||||
|
lifetime.Entities.SetPhysicsBody(record, body);
|
||||||
|
lifetime.Entities.SetFullCell(record, 0xA8B40001u, 0xA8B4FFFFu);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(ResolvedEmptySetup()),
|
||||||
|
out RuntimeSetPositionCommand command));
|
||||||
|
Vector3 priorPosition = body.Position;
|
||||||
|
uint priorCell = record.FullCellId;
|
||||||
|
|
||||||
|
ReplaceAuthority(lifetime, record, replacement);
|
||||||
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition
|
||||||
|
.SubmitPreparedPlacement(token, command);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionStatus.Rejected, outcome.Status);
|
||||||
|
Assert.Equal(priorPosition, body.Position);
|
||||||
|
Assert.Equal(priorCell, record.FullCellId);
|
||||||
|
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
||||||
|
Assert.False(lifetime.Physics.SetPosition
|
||||||
|
.TryGetPreparedMoverSphereCount(record, out _));
|
||||||
|
|
||||||
|
RuntimeEntityPlacementToken replacementToken = Begin(lifetime, record);
|
||||||
|
Assert.True(replacementToken.IsValid);
|
||||||
|
Assert.NotEqual(token, replacementToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThirdAuthoredSphereIsPreservedButNotConsumedByCoreValidation()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
FlatSetupCollision setup = Setup(
|
||||||
|
[
|
||||||
|
new FlatCollisionSphere(Vector3.Zero, 0.1f),
|
||||||
|
new FlatCollisionSphere(Vector3.UnitZ, 0.2f),
|
||||||
|
new FlatCollisionSphere(
|
||||||
|
new Vector3(float.NaN, 0f, 0f),
|
||||||
|
0.3f),
|
||||||
|
], 0f, 0f);
|
||||||
|
|
||||||
|
RuntimeSetPositionMoverPreparationStatus status = lifetime.Physics
|
||||||
|
.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.Resolved(SetupId, setup)),
|
||||||
|
out _);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SessionReplacementInvalidatesExactTokenAndClearsPreparationAuthority()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime);
|
||||||
|
RuntimeEntityPlacementToken token = Begin(lifetime, record);
|
||||||
|
|
||||||
|
_ = lifetime.BeginSessionClear();
|
||||||
|
RuntimeSetPositionMoverPreparationStatus status = lifetime.Physics
|
||||||
|
.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
Input(RuntimeSetPositionMoverSetup.ResolvedAbsent),
|
||||||
|
out RuntimeSetPositionCommand command);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeSetPositionMoverPreparationStatus.RejectedAuthority,
|
||||||
|
status);
|
||||||
|
Assert.Equal(default, command);
|
||||||
|
RuntimeSetPositionOwnershipSnapshot ownership = lifetime.Physics
|
||||||
|
.SetPosition.CaptureOwnership();
|
||||||
|
Assert.Equal(0, ownership.MoverPreparationAuthorityCount);
|
||||||
|
Assert.Equal(0, ownership.ActiveOperationCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeEntityPlacementToken Begin(
|
||||||
|
RuntimeEntityObjectLifetime lifetime,
|
||||||
|
RuntimeEntityRecord record) => lifetime.Physics.SetPosition
|
||||||
|
.BeginAuthoredPlacement(
|
||||||
|
record,
|
||||||
|
record.PositionAuthorityVersion,
|
||||||
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
||||||
|
|
||||||
|
private static RuntimeSetPositionMoverPreparation Input(
|
||||||
|
RuntimeSetPositionMoverSetup setup) => new(
|
||||||
|
setup,
|
||||||
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
||||||
|
GameTime: 1d,
|
||||||
|
PhysicsPlacementClass.Ordinary,
|
||||||
|
PhysicsSetPositionFlags.Placement);
|
||||||
|
|
||||||
|
private static RuntimeSetPositionMoverSetup ResolvedEmptySetup() =>
|
||||||
|
RuntimeSetPositionMoverSetup.Resolved(
|
||||||
|
SetupId,
|
||||||
|
Setup(ImmutableArray<FlatCollisionSphere>.Empty, 0f, 0f));
|
||||||
|
|
||||||
|
private static FlatSetupCollision Setup(
|
||||||
|
ImmutableArray<FlatCollisionSphere> spheres,
|
||||||
|
float stepUp,
|
||||||
|
float stepDown) => new(
|
||||||
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
||||||
|
spheres,
|
||||||
|
height: 0f,
|
||||||
|
radius: 0f,
|
||||||
|
stepUp,
|
||||||
|
stepDown);
|
||||||
|
|
||||||
|
private static RuntimeEntityRecord CreateRecord(
|
||||||
|
RuntimeEntityObjectLifetime lifetime,
|
||||||
|
float? objScale = null,
|
||||||
|
float? physicsScale = null,
|
||||||
|
uint? objectDescriptionFlags = null,
|
||||||
|
uint guid = 0x70002001u,
|
||||||
|
uint? setupTableId = SetupId,
|
||||||
|
CreateObject.ServerPosition? position = null)
|
||||||
|
{
|
||||||
|
CreateObject.ServerPosition acceptedPosition = position ?? new(
|
||||||
|
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: 1);
|
||||||
|
var physics = new PhysicsSpawnData(
|
||||||
|
RawState: (uint)PhysicsStateFlags.Gravity,
|
||||||
|
Position: acceptedPosition,
|
||||||
|
Movement: null,
|
||||||
|
AnimationFrame: null,
|
||||||
|
SetupTableId: setupTableId,
|
||||||
|
MotionTableId: 0x09000001u,
|
||||||
|
SoundTableId: null,
|
||||||
|
PhysicsScriptTableId: null,
|
||||||
|
Parent: null,
|
||||||
|
Children: null,
|
||||||
|
Scale: physicsScale,
|
||||||
|
Friction: null,
|
||||||
|
Elasticity: null,
|
||||||
|
Translucency: null,
|
||||||
|
Velocity: null,
|
||||||
|
Acceleration: null,
|
||||||
|
AngularVelocity: null,
|
||||||
|
DefaultScriptType: null,
|
||||||
|
DefaultScriptIntensity: null,
|
||||||
|
Timestamps: timestamps);
|
||||||
|
var spawn = new WorldSession.EntitySpawn(
|
||||||
|
Guid: guid,
|
||||||
|
Position: acceptedPosition,
|
||||||
|
SetupTableId: setupTableId,
|
||||||
|
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
|
||||||
|
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
|
||||||
|
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||||
|
BasePaletteId: null,
|
||||||
|
ObjScale: objScale,
|
||||||
|
Name: "mover-preparation-fixture",
|
||||||
|
ItemType: null,
|
||||||
|
MotionState: null,
|
||||||
|
MotionTableId: 0x09000001u,
|
||||||
|
PhysicsState: (uint)PhysicsStateFlags.Gravity,
|
||||||
|
ObjectDescriptionFlags: objectDescriptionFlags,
|
||||||
|
InstanceSequence: 1,
|
||||||
|
MovementSequence: 1,
|
||||||
|
ServerControlSequence: 1,
|
||||||
|
PositionSequence: 1,
|
||||||
|
Physics: physics);
|
||||||
|
return lifetime.RegisterEntity(spawn).Canonical!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AttachBody(
|
||||||
|
RuntimeEntityObjectLifetime lifetime,
|
||||||
|
RuntimeEntityRecord record)
|
||||||
|
{
|
||||||
|
var body = new PhysicsBody
|
||||||
|
{
|
||||||
|
Position = new Vector3(1f, 2f, 3f),
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
State = record.FinalPhysicsState,
|
||||||
|
InWorld = true,
|
||||||
|
};
|
||||||
|
body.SnapToCell(Cell, body.Position, body.Position);
|
||||||
|
lifetime.Entities.SetPhysicsBody(record, body);
|
||||||
|
lifetime.Entities.SetFullCell(record, Cell, Cell & 0xFFFF0000u);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReplaceAuthority(
|
||||||
|
RuntimeEntityObjectLifetime lifetime,
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
AuthorityReplacement replacement)
|
||||||
|
{
|
||||||
|
switch (replacement)
|
||||||
|
{
|
||||||
|
case AuthorityReplacement.Position:
|
||||||
|
lifetime.Entities.AdvancePositionAuthority(record);
|
||||||
|
break;
|
||||||
|
case AuthorityReplacement.Velocity:
|
||||||
|
lifetime.Entities.AdvanceMovementAuthority(record);
|
||||||
|
break;
|
||||||
|
case AuthorityReplacement.Vector:
|
||||||
|
lifetime.Entities.AdvanceVectorAuthority(record);
|
||||||
|
break;
|
||||||
|
case AuthorityReplacement.State:
|
||||||
|
_ = lifetime.Entities.ApplyRawPhysicsState(
|
||||||
|
record,
|
||||||
|
(uint)PhysicsStateFlags.Frozen);
|
||||||
|
break;
|
||||||
|
case AuthorityReplacement.ObjectDescription:
|
||||||
|
lifetime.Entities.AdvanceObjDescAuthority(record);
|
||||||
|
break;
|
||||||
|
case AuthorityReplacement.Create:
|
||||||
|
lifetime.Entities.AdvanceCreateAuthority(record);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(replacement));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AuthorityReplacement
|
||||||
|
{
|
||||||
|
Position,
|
||||||
|
Velocity,
|
||||||
|
Vector,
|
||||||
|
State,
|
||||||
|
ObjectDescription,
|
||||||
|
Create,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,7 +57,17 @@ public sealed class RuntimeSetPositionStateTests
|
||||||
record.PositionAuthorityVersion,
|
record.PositionAuthorityVersion,
|
||||||
RuntimeSetPositionOperationKind.LocalAuthoritative,
|
RuntimeSetPositionOperationKind.LocalAuthoritative,
|
||||||
invalidPortal);
|
invalidPortal);
|
||||||
Assert.True(portalToken.IsValid);
|
Assert.False(portalToken.IsValid);
|
||||||
|
|
||||||
|
var matchingPortal = invalidPortal with
|
||||||
|
{
|
||||||
|
Projection = new RuntimeWorldHostProjectionToken(7, SourceCell),
|
||||||
|
};
|
||||||
|
Assert.True(lifetime.Physics.SetPosition.BeginAuthoredPlacement(
|
||||||
|
record,
|
||||||
|
record.PositionAuthorityVersion,
|
||||||
|
RuntimeSetPositionOperationKind.LocalAuthoritative,
|
||||||
|
matchingPortal).IsValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -445,6 +455,146 @@ public sealed class RuntimeSetPositionStateTests
|
||||||
placed.Token));
|
placed.Token));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LegacyDeferredWakeCannotBypassNewAuthoredSealAfterAuthorityStales()
|
||||||
|
{
|
||||||
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000112Bu, 1);
|
||||||
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
||||||
|
var observer = new PlacementObserver();
|
||||||
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
||||||
|
observer);
|
||||||
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
||||||
|
record,
|
||||||
|
record.PositionAuthorityVersion,
|
||||||
|
Command(CrossLandblockRequest()));
|
||||||
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
||||||
|
deferred.Projection));
|
||||||
|
|
||||||
|
// The direct legacy command was valid when parked, but any accepted
|
||||||
|
// authority replacement requires a new authored mover preparation.
|
||||||
|
lifetime.Entities.AdvanceObjDescAuthority(record);
|
||||||
|
AddFlatLandblock(engine, DestinationLandblock, 192f);
|
||||||
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
||||||
|
DestinationLandblock,
|
||||||
|
generation: 1,
|
||||||
|
ready: true);
|
||||||
|
|
||||||
|
Assert.False(body.InWorld);
|
||||||
|
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
|
||||||
|
record,
|
||||||
|
out RuntimeEntityPlacementToken token));
|
||||||
|
Assert.Equal(RuntimeEntityPlacementPreparationKind.LegacyDirect,
|
||||||
|
token.PreparationKind);
|
||||||
|
RuntimeSetPositionOutcome bypass = lifetime.Physics.SetPosition
|
||||||
|
.SubmitPreparedPlacement(token, Command(CrossLandblockRequest()));
|
||||||
|
Assert.Equal(RuntimeSetPositionStatus.Rejected, bypass.Status);
|
||||||
|
Assert.False(body.InWorld);
|
||||||
|
|
||||||
|
RuntimeSetPositionCommand prepared = PrepareAuthoredCommand(
|
||||||
|
lifetime,
|
||||||
|
token,
|
||||||
|
[new FlatCollisionSphere(Vector3.Zero, 0.4f)]);
|
||||||
|
RuntimeSetPositionOutcome resumed = lifetime.Physics.SetPosition
|
||||||
|
.SubmitPreparedPlacement(token, prepared);
|
||||||
|
Assert.Equal(PhysicsResidenceDisposition.Committed, resumed.Residence);
|
||||||
|
RuntimePlacementProjectionSnapshot placed = observer.Deltas[^1].Placement;
|
||||||
|
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
||||||
|
Assert.True(body.InWorld);
|
||||||
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
||||||
|
placed.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(DeferredPhysicsStateMutation.ChildNoDraw)]
|
||||||
|
[InlineData(DeferredPhysicsStateMutation.StopMissile)]
|
||||||
|
[InlineData(DeferredPhysicsStateMutation.DirectFinalState)]
|
||||||
|
public void DeferredWakeRequiresRepreparationAfterFinalPhysicsStateMutation(
|
||||||
|
DeferredPhysicsStateMutation mutation)
|
||||||
|
{
|
||||||
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000112Cu, 1);
|
||||||
|
PhysicsStateFlags initial = PhysicsStateFlags.Gravity;
|
||||||
|
if (mutation is DeferredPhysicsStateMutation.StopMissile)
|
||||||
|
{
|
||||||
|
initial |= PhysicsStateFlags.Missile
|
||||||
|
| PhysicsStateFlags.AlignPath
|
||||||
|
| PhysicsStateFlags.PathClipped;
|
||||||
|
}
|
||||||
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell, initial);
|
||||||
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
||||||
|
record,
|
||||||
|
record.PositionAuthorityVersion,
|
||||||
|
Command(CrossLandblockRequest()));
|
||||||
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
||||||
|
deferred.Projection));
|
||||||
|
ulong priorMutation = record.PhysicsStateMutationVersion;
|
||||||
|
|
||||||
|
switch (mutation)
|
||||||
|
{
|
||||||
|
case DeferredPhysicsStateMutation.ChildNoDraw:
|
||||||
|
lifetime.Entities.SetChildNoDraw(record, noDraw: true);
|
||||||
|
break;
|
||||||
|
case DeferredPhysicsStateMutation.StopMissile:
|
||||||
|
Assert.True(lifetime.Entities.StopMissileAfterCollision(
|
||||||
|
record,
|
||||||
|
requireCurrentMissile: true));
|
||||||
|
break;
|
||||||
|
case DeferredPhysicsStateMutation.DirectFinalState:
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(
|
||||||
|
record,
|
||||||
|
record.FinalPhysicsState | PhysicsStateFlags.Frozen);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(mutation));
|
||||||
|
}
|
||||||
|
Assert.Equal(priorMutation + 1UL,
|
||||||
|
record.PhysicsStateMutationVersion);
|
||||||
|
if (mutation is DeferredPhysicsStateMutation.DirectFinalState)
|
||||||
|
Assert.NotEqual(record.FinalPhysicsState, body.State);
|
||||||
|
else
|
||||||
|
Assert.Equal(record.FinalPhysicsState, body.State);
|
||||||
|
|
||||||
|
AddFlatLandblock(engine, DestinationLandblock, 192f);
|
||||||
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
||||||
|
DestinationLandblock,
|
||||||
|
generation: 1,
|
||||||
|
ready: true);
|
||||||
|
|
||||||
|
Assert.False(body.InWorld);
|
||||||
|
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
|
||||||
|
record,
|
||||||
|
out _));
|
||||||
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
||||||
|
.AwaitingSetPositionPreparationCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DirectFinalPhysicsStateMutationAdvancesOnlyOnChangeAndLeavesBodyToItsOwner()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000112Du, 1);
|
||||||
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
||||||
|
PhysicsStateFlags original = record.FinalPhysicsState;
|
||||||
|
ulong version = record.PhysicsStateMutationVersion;
|
||||||
|
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(record, original);
|
||||||
|
Assert.Equal(version, record.PhysicsStateMutationVersion);
|
||||||
|
Assert.Equal(original, body.State);
|
||||||
|
|
||||||
|
PhysicsStateFlags changed = original | PhysicsStateFlags.Frozen;
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(record, changed);
|
||||||
|
Assert.Equal(version + 1UL, record.PhysicsStateMutationVersion);
|
||||||
|
Assert.Equal(changed, record.FinalPhysicsState);
|
||||||
|
Assert.Equal(original, body.State);
|
||||||
|
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(record, changed);
|
||||||
|
Assert.Equal(version + 1UL, record.PhysicsStateMutationVersion);
|
||||||
|
Assert.Equal(original, body.State);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RejectedPreparationKeepsValidatedMoverAndExactTokenRetryable()
|
public void RejectedPreparationKeepsValidatedMoverAndExactTokenRetryable()
|
||||||
{
|
{
|
||||||
|
|
@ -1943,15 +2093,12 @@ public sealed class RuntimeSetPositionStateTests
|
||||||
new FlatCollisionSphere(new Vector3(0.4f, 0f, 0f), 0.35f),
|
new FlatCollisionSphere(new Vector3(0.4f, 0f, 0f), 0.35f),
|
||||||
new FlatCollisionSphere(new Vector3(-0.3f, 0f, 0.5f), 0.2f),
|
new FlatCollisionSphere(new Vector3(-0.3f, 0f, 0.5f), 0.2f),
|
||||||
];
|
];
|
||||||
|
RuntimeSetPositionCommand preparedCommand = PrepareAuthoredCommand(
|
||||||
|
lifetime,
|
||||||
|
token,
|
||||||
|
authored);
|
||||||
RuntimeSetPositionOutcome supplied = lifetime.Physics.SetPosition
|
RuntimeSetPositionOutcome supplied = lifetime.Physics.SetPosition
|
||||||
.SubmitPreparedPlacement(
|
.SubmitPreparedPlacement(token, preparedCommand);
|
||||||
token,
|
|
||||||
Command(Request(
|
|
||||||
landblock | 0x0101u,
|
|
||||||
new Vector3(999f, 999f, 999f)) with
|
|
||||||
{
|
|
||||||
Spheres = authored,
|
|
||||||
}));
|
|
||||||
|
|
||||||
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, supplied.Status);
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, supplied.Status);
|
||||||
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
||||||
|
|
@ -1998,21 +2145,29 @@ public sealed class RuntimeSetPositionStateTests
|
||||||
Assert.Single(observer.Deltas).Placement.Token));
|
Assert.Single(observer.Deltas).Placement.Token));
|
||||||
observer.Deltas.Clear();
|
observer.Deltas.Clear();
|
||||||
|
|
||||||
RuntimeSetPositionCommand malformed = Command(Request(
|
var malformedSetup = new FlatSetupCollision(
|
||||||
exactCell,
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
||||||
new Vector3(10f, 18f, 7f)) with
|
[new FlatCollisionSphere(
|
||||||
{
|
new Vector3(float.NaN, 0f, 0f),
|
||||||
Spheres =
|
0.4f)],
|
||||||
[
|
height: 0f,
|
||||||
new FlatCollisionSphere(
|
radius: 0f,
|
||||||
new Vector3(float.NaN, 0f, 0f),
|
stepUpHeight: 0.4f,
|
||||||
0.4f),
|
stepDownHeight: 0.4f);
|
||||||
],
|
Assert.Equal(
|
||||||
});
|
RuntimeSetPositionMoverPreparationStatus.InvalidData,
|
||||||
RuntimeSetPositionOutcome acceptedMalformed = lifetime.Physics
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
.SetPosition.SubmitPreparedPlacement(token, malformed);
|
token,
|
||||||
Assert.Equal(RuntimeSetPositionStatus.DeferredCell,
|
new RuntimeSetPositionMoverPreparation(
|
||||||
acceptedMalformed.Status);
|
RuntimeSetPositionMoverSetup.Resolved(
|
||||||
|
0x02000001u,
|
||||||
|
malformedSetup),
|
||||||
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
||||||
|
GameTime: 10d,
|
||||||
|
PhysicsPlacementClass.Ordinary,
|
||||||
|
PhysicsSetPositionFlags.Placement
|
||||||
|
| PhysicsSetPositionFlags.Slide),
|
||||||
|
out _));
|
||||||
|
|
||||||
RuntimeCollisionAdmission first = lifetime.Physics
|
RuntimeCollisionAdmission first = lifetime.Physics
|
||||||
.BeginCollisionAdmission(landblock);
|
.BeginCollisionAdmission(landblock);
|
||||||
|
|
@ -2032,44 +2187,25 @@ public sealed class RuntimeSetPositionStateTests
|
||||||
RuntimeSetPositionOwnershipSnapshot invalidWake = lifetime.Physics
|
RuntimeSetPositionOwnershipSnapshot invalidWake = lifetime.Physics
|
||||||
.SetPosition.CaptureOwnership();
|
.SetPosition.CaptureOwnership();
|
||||||
Assert.Equal(1, invalidWake.AwaitingPreparationCount);
|
Assert.Equal(1, invalidWake.AwaitingPreparationCount);
|
||||||
Assert.Equal(1, invalidWake.DeferredBucketCount);
|
Assert.Equal(0, invalidWake.DeferredBucketCount);
|
||||||
Assert.Equal(1, invalidWake.LostDeadlineCount);
|
Assert.Equal(1, invalidWake.LostDeadlineCount);
|
||||||
Assert.Empty(observer.Deltas);
|
Assert.Empty(observer.Deltas);
|
||||||
Assert.False(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
Assert.False(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
||||||
record,
|
record,
|
||||||
out _));
|
out _));
|
||||||
|
|
||||||
RuntimeSetPositionCommand corrected = Command(Request(
|
RuntimeSetPositionCommand corrected = PrepareAuthoredCommand(
|
||||||
exactCell,
|
lifetime,
|
||||||
new Vector3(10f, 18f, 7f)) with
|
token,
|
||||||
{
|
[new FlatCollisionSphere(Vector3.Zero, 0.4f)]);
|
||||||
Spheres =
|
|
||||||
[
|
|
||||||
new FlatCollisionSphere(Vector3.Zero, 0.4f),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
RuntimeSetPositionOutcome correctedPending = lifetime.Physics
|
RuntimeSetPositionOutcome correctedPending = lifetime.Physics
|
||||||
.SetPosition.SubmitPreparedPlacement(token, corrected);
|
.SetPosition.SubmitPreparedPlacement(token, corrected);
|
||||||
Assert.Equal(RuntimeSetPositionStatus.DeferredCell,
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell,
|
||||||
correctedPending.Status);
|
correctedPending.Status);
|
||||||
Assert.Equal(PhysicsResidenceDisposition.DeferredCell,
|
Assert.Equal(PhysicsResidenceDisposition.Committed,
|
||||||
correctedPending.Residence);
|
correctedPending.Residence);
|
||||||
Assert.Equal(exactCell, correctedPending.ExactCellId);
|
Assert.Equal(exactCell, correctedPending.ExactCellId);
|
||||||
|
|
||||||
RuntimeCollisionAdmission second = lifetime.Physics
|
|
||||||
.BeginCollisionAdmission(landblock);
|
|
||||||
using PreparedLandblockCollisionGeneration preparedSecond = lifetime
|
|
||||||
.Physics.PrepareCollisionGeneration(second);
|
|
||||||
lifetime.Physics.StageCollisionAssets(
|
|
||||||
second,
|
|
||||||
preparedSecond,
|
|
||||||
CollisionAssets(landblock));
|
|
||||||
AddSyntheticCell(preparedSecond.DataCache, exactCell);
|
|
||||||
Assert.True(CommitPrepared(
|
|
||||||
lifetime.Physics,
|
|
||||||
second,
|
|
||||||
preparedSecond).Committed);
|
|
||||||
|
|
||||||
RuntimePlacementProjectionSnapshot placed = Assert.Single(observer.Deltas)
|
RuntimePlacementProjectionSnapshot placed = Assert.Single(observer.Deltas)
|
||||||
.Placement;
|
.Placement;
|
||||||
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
||||||
|
|
@ -2178,6 +2314,34 @@ public sealed class RuntimeSetPositionStateTests
|
||||||
ShadowWorldOffsetX: request.CellId == SourceCell ? 0f : 192f,
|
ShadowWorldOffsetX: request.CellId == SourceCell ? 0f : 192f,
|
||||||
ShadowWorldOffsetY: 0f);
|
ShadowWorldOffsetY: 0f);
|
||||||
|
|
||||||
|
private static RuntimeSetPositionCommand PrepareAuthoredCommand(
|
||||||
|
RuntimeEntityObjectLifetime lifetime,
|
||||||
|
RuntimeEntityPlacementToken token,
|
||||||
|
ImmutableArray<FlatCollisionSphere> spheres)
|
||||||
|
{
|
||||||
|
var setup = new FlatSetupCollision(
|
||||||
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
||||||
|
spheres,
|
||||||
|
height: 0f,
|
||||||
|
radius: 0f,
|
||||||
|
stepUpHeight: 0.4f,
|
||||||
|
stepDownHeight: 0.4f);
|
||||||
|
var preparation = new RuntimeSetPositionMoverPreparation(
|
||||||
|
RuntimeSetPositionMoverSetup.Resolved(0x02000001u, setup),
|
||||||
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
||||||
|
GameTime: 10d,
|
||||||
|
PhysicsPlacementClass.Ordinary,
|
||||||
|
PhysicsSetPositionFlags.Placement
|
||||||
|
| PhysicsSetPositionFlags.Slide);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
token,
|
||||||
|
preparation,
|
||||||
|
out RuntimeSetPositionCommand command));
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
private static PhysicsSetPositionRequest CrossLandblockRequest() =>
|
private static PhysicsSetPositionRequest CrossLandblockRequest() =>
|
||||||
Request(
|
Request(
|
||||||
SourceCell,
|
SourceCell,
|
||||||
|
|
@ -2480,6 +2644,13 @@ public sealed class RuntimeSetPositionStateTests
|
||||||
internal void JumpUtc(TimeSpan duration) => _utcNow += duration;
|
internal void JumpUtc(TimeSpan duration) => _utcNow += duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum DeferredPhysicsStateMutation
|
||||||
|
{
|
||||||
|
ChildNoDraw,
|
||||||
|
StopMissile,
|
||||||
|
DirectFinalState,
|
||||||
|
}
|
||||||
|
|
||||||
private enum CancellationChannel : uint
|
private enum CancellationChannel : uint
|
||||||
{
|
{
|
||||||
Position,
|
Position,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue