refactor(runtime): own local movement and outbound cadence
Move the canonical local movement controller, body/motion managers, object clock, movement wire data, and MTS/jump/AP sender into AcDream.Runtime. Replace process skill defaults with typed Runtime character options, make graphical and direct commands borrow one autorun owner, retain the construction-time PartArray seam, and include movement in terminal ownership convergence. Preserve the accepted pre-inbound movement/jump and post-inbound autonomous-position order while moving the exact packet/cadence fixtures into Runtime tests. Add graphical/direct parity, two-instance isolation, teardown, allocation, architecture, and divergence-path coverage. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
3456dff038
commit
aa3f4a60f8
36 changed files with 878 additions and 276 deletions
|
|
@ -9,6 +9,9 @@
|
|||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="AcDream.Runtime.Tests" />
|
||||
<InternalsVisibleTo Include="AcDream.App" />
|
||||
<InternalsVisibleTo Include="AcDream.App.Tests" />
|
||||
<InternalsVisibleTo Include="AcDream.Core.Tests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
||||
|
|
|
|||
|
|
@ -73,7 +73,9 @@ public readonly record struct RuntimeMovementSnapshot(
|
|||
Position Position,
|
||||
System.Numerics.Vector3 Velocity,
|
||||
bool IsAirborne,
|
||||
double SimulationTimeSeconds);
|
||||
double SimulationTimeSeconds,
|
||||
long Revision = 0,
|
||||
bool AutoRunActive = false);
|
||||
|
||||
public interface IRuntimeMovementView
|
||||
{
|
||||
|
|
|
|||
299
src/AcDream.Runtime/Gameplay/LocalPlayerOutboundController.cs
Normal file
299
src/AcDream.Runtime/Gameplay/LocalPlayerOutboundController.cs
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
public interface IMovementTruthDiagnosticSink
|
||||
{
|
||||
void OnOutbound(
|
||||
string kind,
|
||||
uint sequence,
|
||||
MovementResult result,
|
||||
Vector3 wirePosition,
|
||||
uint wireCellId,
|
||||
byte contactByte);
|
||||
|
||||
void OnServerEcho(
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
Vector3 serverWorldPosition);
|
||||
|
||||
void ResetSession();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes input-originated output from the object-phase result and the
|
||||
/// periodic position report from post-inbound controller state.
|
||||
/// </summary>
|
||||
public sealed class LocalPlayerOutboundController
|
||||
{
|
||||
private readonly IMovementTruthDiagnosticSink _diagnostic;
|
||||
|
||||
internal LocalPlayerOutboundController(IMovementTruthDiagnosticSink diagnostic)
|
||||
{
|
||||
_diagnostic = diagnostic
|
||||
?? throw new ArgumentNullException(nameof(diagnostic));
|
||||
}
|
||||
|
||||
public LocalPlayerOutboundController(
|
||||
Action<string, uint, MovementResult, Vector3, uint, byte> diagnostic)
|
||||
{
|
||||
_diagnostic = new DelegateMovementTruthDiagnosticSink(diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends input-originated movement and jump packets before inbound
|
||||
/// dispatch. Retail emits these from input/object processing rather than
|
||||
/// from <c>CommandInterpreter::UseTime</c>.
|
||||
/// </summary>
|
||||
public void SendPreNetworkActions(
|
||||
WorldSession? session,
|
||||
PlayerMovementController controller,
|
||||
MovementResult movement,
|
||||
bool hidden)
|
||||
{
|
||||
if (session is null || hidden)
|
||||
return;
|
||||
|
||||
if (!controller.TryGetOutboundPosition(out Position outboundPosition))
|
||||
{
|
||||
return;
|
||||
}
|
||||
uint wireCellId = outboundPosition.ObjCellId;
|
||||
Vector3 wirePosition = outboundPosition.Frame.Origin;
|
||||
Quaternion wireRotation = outboundPosition.Frame.Orientation;
|
||||
|
||||
if (movement.ShouldSendMovementEvent)
|
||||
TrySendMovement(session, controller, movement);
|
||||
|
||||
if (movement.JumpExtent.HasValue && movement.JumpVelocity.HasValue)
|
||||
{
|
||||
uint sequence = session.NextGameActionSequence();
|
||||
byte[] body = JumpAction.Build(
|
||||
gameActionSequence: sequence,
|
||||
extent: movement.JumpExtent.Value,
|
||||
velocity: movement.JumpVelocity.Value,
|
||||
cellId: wireCellId,
|
||||
position: wirePosition,
|
||||
rotation: wireRotation,
|
||||
instanceSequence: session.InstanceSequence,
|
||||
serverControlSequence: session.ServerControlSequence,
|
||||
teleportSequence: session.TeleportSequence,
|
||||
forcePositionSequence: session.ForcePositionSequence);
|
||||
session.SendGameAction(body);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ports the position-send slot in <c>CommandInterpreter::UseTime</c>
|
||||
/// (<c>0x006B3BF0</c>), which SmartBox calls after draining inbound
|
||||
/// events. The predicate and serialized frame therefore observe any
|
||||
/// accepted ForcePosition or teleport state from this update.
|
||||
/// </summary>
|
||||
public void SendPostNetworkPosition(
|
||||
WorldSession? session,
|
||||
PlayerMovementController controller,
|
||||
bool hidden)
|
||||
{
|
||||
if (session is null || hidden)
|
||||
return;
|
||||
|
||||
if (!controller.TryGetOutboundPosition(out Position position))
|
||||
{
|
||||
return;
|
||||
}
|
||||
uint wireCellId = position.ObjCellId;
|
||||
Vector3 wirePosition = position.Frame.Origin;
|
||||
Quaternion wireRotation = position.Frame.Orientation;
|
||||
|
||||
if (!controller.ShouldSendPositionEvent(
|
||||
position,
|
||||
controller.ContactPlane,
|
||||
controller.SimTimeSeconds)
|
||||
|| !controller.CanSendPositionEvent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MovementResult movement = controller.CapturePresentationResult();
|
||||
byte contactByte = movement.IsOnGround ? (byte)1 : (byte)0;
|
||||
uint sequence = session.NextGameActionSequence();
|
||||
byte[] body = AutonomousPosition.Build(
|
||||
gameActionSequence: sequence,
|
||||
cellId: wireCellId,
|
||||
position: wirePosition,
|
||||
rotation: wireRotation,
|
||||
instanceSequence: session.InstanceSequence,
|
||||
serverControlSequence: session.ServerControlSequence,
|
||||
teleportSequence: session.TeleportSequence,
|
||||
forcePositionSequence: session.ForcePositionSequence,
|
||||
lastContact: contactByte);
|
||||
_diagnostic.OnOutbound(
|
||||
"AP",
|
||||
sequence,
|
||||
movement,
|
||||
wirePosition,
|
||||
wireCellId,
|
||||
contactByte);
|
||||
session.SendGameAction(body);
|
||||
controller.NotePositionSent(
|
||||
position,
|
||||
controller.ContactPlane,
|
||||
controller.SimTimeSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail ForcePosition acknowledgement sent immediately after the local
|
||||
/// body is blipped. Unlike the periodic post-network sender, this bypasses
|
||||
/// the elapsed/difference predicate but still requires a valid grounded
|
||||
/// canonical frame.
|
||||
/// </summary>
|
||||
internal void SendImmediatePosition(
|
||||
WorldSession? session,
|
||||
PlayerMovementController? controller)
|
||||
{
|
||||
if (session is null
|
||||
|| controller is null
|
||||
|| !controller.CanSendPositionEvent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!controller.TryGetOutboundPosition(out Position outboundPosition))
|
||||
return;
|
||||
uint cellId = outboundPosition.ObjCellId;
|
||||
Vector3 position = outboundPosition.Frame.Origin;
|
||||
Quaternion rotation = outboundPosition.Frame.Orientation;
|
||||
|
||||
uint sequence = session.NextGameActionSequence();
|
||||
byte[] body = AutonomousPosition.Build(
|
||||
gameActionSequence: sequence,
|
||||
cellId: cellId,
|
||||
position: position,
|
||||
rotation: rotation,
|
||||
instanceSequence: session.InstanceSequence,
|
||||
serverControlSequence: session.ServerControlSequence,
|
||||
teleportSequence: session.TeleportSequence,
|
||||
forcePositionSequence: session.ForcePositionSequence,
|
||||
lastContact: 1);
|
||||
session.SendGameAction(body);
|
||||
controller.NotePositionSent(
|
||||
outboundPosition,
|
||||
controller.ContactPlane,
|
||||
controller.SimTimeSeconds);
|
||||
}
|
||||
|
||||
public bool TrySendMovement(
|
||||
WorldSession? session,
|
||||
PlayerMovementController? controller,
|
||||
MovementResult movement)
|
||||
{
|
||||
if (session is null || controller is null)
|
||||
return false;
|
||||
|
||||
if (!controller.TryGetOutboundPosition(out Position outboundPosition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
uint wireCellId = outboundPosition.ObjCellId;
|
||||
Vector3 wirePosition = outboundPosition.Frame.Origin;
|
||||
Quaternion wireRotation = outboundPosition.Frame.Orientation;
|
||||
|
||||
byte contactByte = movement.IsOnGround ? (byte)1 : (byte)0;
|
||||
RawMotionState rawMotionState = BuildRawMotionState(movement);
|
||||
uint sequence = session.NextGameActionSequence();
|
||||
byte[] body = MoveToState.Build(
|
||||
gameActionSequence: sequence,
|
||||
rawMotionState: rawMotionState,
|
||||
cellId: wireCellId,
|
||||
position: wirePosition,
|
||||
rotation: wireRotation,
|
||||
instanceSequence: session.InstanceSequence,
|
||||
serverControlSequence: session.ServerControlSequence,
|
||||
teleportSequence: session.TeleportSequence,
|
||||
forcePositionSequence: session.ForcePositionSequence,
|
||||
contact: contactByte != 0,
|
||||
standingLongjump: false);
|
||||
_diagnostic.OnOutbound(
|
||||
"MTS",
|
||||
sequence,
|
||||
movement,
|
||||
wirePosition,
|
||||
wireCellId,
|
||||
contactByte);
|
||||
session.SendGameAction(body);
|
||||
controller.NoteMovementSent(
|
||||
controller.SimTimeSeconds,
|
||||
movement.IsMouseLookMovementEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static RawMotionState BuildRawMotionState(MovementResult movement)
|
||||
{
|
||||
HoldKey axisHoldKey = movement.IsRunning ? HoldKey.Run : HoldKey.None;
|
||||
return new RawMotionState
|
||||
{
|
||||
CurrentHoldKey = axisHoldKey,
|
||||
// CommandInterpreter::SendMovementEvent @ 0x006B4680 passes
|
||||
// CPhysicsObj::InqRawMotionState directly to MoveToStatePack.
|
||||
// RawMotionState::UnPack @ 0x0051EFC0 treats an absent style as
|
||||
// NonCombat, so this is state—not optional observer decoration.
|
||||
CurrentStyle = movement.CurrentStyle,
|
||||
ForwardCommand = movement.ForwardCommand
|
||||
?? RawMotionState.Default.ForwardCommand,
|
||||
ForwardHoldKey = movement.ForwardCommand.HasValue
|
||||
? axisHoldKey : HoldKey.Invalid,
|
||||
ForwardSpeed = movement.ForwardSpeed
|
||||
?? RawMotionState.Default.ForwardSpeed,
|
||||
SidestepCommand = movement.SidestepCommand
|
||||
?? RawMotionState.Default.SidestepCommand,
|
||||
SidestepHoldKey = movement.SidestepCommand.HasValue
|
||||
? movement.SidestepUsesRunHold
|
||||
? HoldKey.Run
|
||||
: axisHoldKey
|
||||
: HoldKey.Invalid,
|
||||
SidestepSpeed = movement.SidestepSpeed
|
||||
?? RawMotionState.Default.SidestepSpeed,
|
||||
TurnCommand = movement.TurnCommand
|
||||
?? RawMotionState.Default.TurnCommand,
|
||||
TurnHoldKey = movement.TurnCommand.HasValue
|
||||
? movement.TurnUsesRunHold
|
||||
? HoldKey.Run
|
||||
: axisHoldKey
|
||||
: HoldKey.Invalid,
|
||||
TurnSpeed = movement.TurnSpeed
|
||||
?? RawMotionState.Default.TurnSpeed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DelegateMovementTruthDiagnosticSink
|
||||
: IMovementTruthDiagnosticSink
|
||||
{
|
||||
private readonly Action<string, uint, MovementResult, Vector3, uint, byte>
|
||||
_outbound;
|
||||
|
||||
public DelegateMovementTruthDiagnosticSink(
|
||||
Action<string, uint, MovementResult, Vector3, uint, byte> outbound) =>
|
||||
_outbound = outbound ?? throw new ArgumentNullException(nameof(outbound));
|
||||
|
||||
public void OnOutbound(
|
||||
string kind,
|
||||
uint sequence,
|
||||
MovementResult result,
|
||||
Vector3 wirePosition,
|
||||
uint wireCellId,
|
||||
byte contactByte) =>
|
||||
_outbound(kind, sequence, result, wirePosition, wireCellId, contactByte);
|
||||
|
||||
public void OnServerEcho(
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
Vector3 serverWorldPosition)
|
||||
{
|
||||
}
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
}
|
||||
}
|
||||
2079
src/AcDream.Runtime/Gameplay/PlayerMovementController.cs
Normal file
2079
src/AcDream.Runtime/Gameplay/PlayerMovementController.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,20 +2,22 @@ namespace AcDream.Runtime.Gameplay;
|
|||
|
||||
/// <summary>
|
||||
/// One allocation-free ledger over the gameplay-state lifetime group through
|
||||
/// J5.1. It contains no state of its own; graphical and no-window hosts capture
|
||||
/// the same four canonical owners.
|
||||
/// J5.4. It contains no state of its own; graphical and no-window hosts capture
|
||||
/// the same canonical owners.
|
||||
/// </summary>
|
||||
public readonly record struct RuntimeGameplayOwnershipSnapshot(
|
||||
RuntimeInventoryOwnershipSnapshot Inventory,
|
||||
RuntimeCharacterOwnershipSnapshot Character,
|
||||
RuntimeCommunicationOwnershipSnapshot Communication,
|
||||
RuntimeActionOwnershipSnapshot Actions)
|
||||
RuntimeActionOwnershipSnapshot Actions,
|
||||
RuntimeLocalMovementOwnershipSnapshot Movement)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
Inventory.IsConverged
|
||||
&& Character.IsConverged
|
||||
&& Communication.IsConverged
|
||||
&& Actions.IsConverged;
|
||||
&& Actions.IsConverged
|
||||
&& Movement.IsConverged;
|
||||
}
|
||||
|
||||
public static class RuntimeGameplayOwnership
|
||||
|
|
@ -24,16 +26,19 @@ public static class RuntimeGameplayOwnership
|
|||
RuntimeInventoryState inventory,
|
||||
RuntimeCharacterState character,
|
||||
RuntimeCommunicationState communication,
|
||||
RuntimeActionState actions)
|
||||
RuntimeActionState actions,
|
||||
RuntimeLocalPlayerMovementState movement)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(inventory);
|
||||
ArgumentNullException.ThrowIfNull(character);
|
||||
ArgumentNullException.ThrowIfNull(communication);
|
||||
ArgumentNullException.ThrowIfNull(actions);
|
||||
ArgumentNullException.ThrowIfNull(movement);
|
||||
return new RuntimeGameplayOwnershipSnapshot(
|
||||
inventory.CaptureOwnership(),
|
||||
character.CaptureOwnership(),
|
||||
communication.CaptureOwnership(),
|
||||
actions.CaptureOwnership());
|
||||
actions.CaptureOwnership(),
|
||||
movement.CaptureOwnership());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
200
src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerMovementState.cs
Normal file
200
src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerMovementState.cs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
public interface IRuntimeLocalPlayerControllerSource
|
||||
{
|
||||
PlayerMovementController? Controller { get; }
|
||||
}
|
||||
|
||||
public interface IRuntimeLocalPlayerMotionSource
|
||||
{
|
||||
MotionInterpreter? Motion { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonical local movement lifetime and intent owner. Graphical input,
|
||||
/// presentation, diagnostics, and future no-window hosts borrow this exact
|
||||
/// state; they never mirror the controller or the autorun latch.
|
||||
/// </summary>
|
||||
public sealed class RuntimeLocalPlayerMovementState
|
||||
: IRuntimeLocalPlayerControllerSource,
|
||||
IRuntimeLocalPlayerMotionSource,
|
||||
IRuntimeMovementView,
|
||||
IDisposable
|
||||
{
|
||||
private PlayerMovementController? _controller;
|
||||
private PlayerMovementController? _preparingMotionOwner;
|
||||
private bool _autoRunActive;
|
||||
private bool _disposed;
|
||||
private long _revision;
|
||||
|
||||
public PlayerMovementController? Controller
|
||||
{
|
||||
get => _controller;
|
||||
set
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (ReferenceEquals(_controller, value))
|
||||
return;
|
||||
_controller = value;
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutoRunActive => _autoRunActive;
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
public IRuntimeMovementView View => this;
|
||||
|
||||
MotionInterpreter? IRuntimeLocalPlayerMotionSource.Motion =>
|
||||
_preparingMotionOwner?.Motion ?? _controller?.Motion;
|
||||
|
||||
public RuntimeMovementSnapshot Snapshot
|
||||
{
|
||||
get
|
||||
{
|
||||
PlayerMovementController? controller = _controller;
|
||||
return controller is null
|
||||
? new RuntimeMovementSnapshot(
|
||||
false,
|
||||
0u,
|
||||
default,
|
||||
default,
|
||||
false,
|
||||
0d,
|
||||
Revision,
|
||||
_autoRunActive)
|
||||
: new RuntimeMovementSnapshot(
|
||||
true,
|
||||
controller.LocalEntityId,
|
||||
controller.CellPosition,
|
||||
controller.BodyVelocity,
|
||||
controller.IsAirborne,
|
||||
controller.SimTimeSeconds,
|
||||
Revision,
|
||||
_autoRunActive);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the retail construction seam where a PartArray completion can
|
||||
/// reach the candidate MotionInterpreter before the controller is
|
||||
/// published to ordinary borrowers.
|
||||
/// </summary>
|
||||
public IDisposable BeginMotionPreparation(
|
||||
PlayerMovementController controller,
|
||||
Action? drainPriorAnimationQueue = null)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
if (_preparingMotionOwner is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A local player motion owner is already being prepared.");
|
||||
}
|
||||
|
||||
drainPriorAnimationQueue?.Invoke();
|
||||
_preparingMotionOwner = controller;
|
||||
return new MotionPreparation(this, controller);
|
||||
}
|
||||
|
||||
public bool Execute(RuntimeMovementCommand command)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
switch (command)
|
||||
{
|
||||
case RuntimeMovementCommand.ToggleRunLock:
|
||||
_autoRunActive = !_autoRunActive;
|
||||
Interlocked.Increment(ref _revision);
|
||||
return true;
|
||||
case RuntimeMovementCommand.Stop:
|
||||
CancelAutoRun();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CancelAutoRun()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_autoRunActive)
|
||||
return false;
|
||||
_autoRunActive = false;
|
||||
Interlocked.Increment(ref _revision);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ResetInputIntent()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_autoRunActive)
|
||||
return;
|
||||
_autoRunActive = false;
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
public RuntimeLocalMovementOwnershipSnapshot CaptureOwnership() =>
|
||||
new(
|
||||
_disposed,
|
||||
_controller is not null,
|
||||
_preparingMotionOwner is not null,
|
||||
_autoRunActive,
|
||||
Revision);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_autoRunActive = false;
|
||||
_controller = null;
|
||||
_preparingMotionOwner = null;
|
||||
Interlocked.Increment(ref _revision);
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void EndMotionPreparation(PlayerMovementController controller)
|
||||
{
|
||||
// Terminal disposal clears the unpublished construction seam. A
|
||||
// caller may still unwind the already-issued lease afterward; that
|
||||
// unwind is idempotent rather than resurrecting or faulting the
|
||||
// retired runtime owner.
|
||||
if (_disposed && _preparingMotionOwner is null)
|
||||
return;
|
||||
|
||||
if (!ReferenceEquals(_preparingMotionOwner, controller))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The local player motion preparation owner changed unexpectedly.");
|
||||
}
|
||||
_preparingMotionOwner = null;
|
||||
}
|
||||
|
||||
private sealed class MotionPreparation(
|
||||
RuntimeLocalPlayerMovementState owner,
|
||||
PlayerMovementController controller) : IDisposable
|
||||
{
|
||||
private RuntimeLocalPlayerMovementState? _owner = owner;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
RuntimeLocalPlayerMovementState? current =
|
||||
Interlocked.Exchange(ref _owner, null);
|
||||
current?.EndMotionPreparation(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeLocalMovementOwnershipSnapshot(
|
||||
bool IsDisposed,
|
||||
bool HasController,
|
||||
bool HasPreparingMotionOwner,
|
||||
bool AutoRunActive,
|
||||
long Revision)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
&& !HasController
|
||||
&& !HasPreparingMotionOwner
|
||||
&& !AutoRunActive;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue