From cdee7a4b49addb5e1500753f6a885f7c899bd0f0 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 26 Jul 2026 15:53:31 +0200 Subject: [PATCH] refactor(runtime): close simulation ownership Move remote-motion construction, CreateObject vector initialization, final simulation-component retirement, and the combined J5 ownership ledger into Runtime. Delete App compatibility views and moved-state reconstruction while preserving the existing graphical projection and retail update order. --- .../Combat/LiveCombatAttackOperations.cs | 4 +- .../Composition/FrameRootComposition.cs | 2 +- .../InteractionRetainedUiComposition.cs | 2 +- .../LivePresentationComposition.cs | 2 +- .../Composition/SessionPlayerComposition.cs | 4 +- .../RuntimeDiagnosticCommandController.cs | 4 +- src/AcDream.App/GlobalUsings.cs | 14 - .../Input/LocalPlayerFrameRuntime.cs | 4 +- src/AcDream.App/Input/MouseLookController.cs | 4 +- .../MovementTruthDiagnosticController.cs | 4 +- src/AcDream.App/Input/PlayerModeController.cs | 4 +- .../Net/LiveSessionRuntimeFactory.cs | 2 +- .../LiveEntityNetworkUpdateController.cs | 81 ++-- .../Physics/LiveEntityShadowPublisher.cs | 4 +- .../Physics/ProjectileController.cs | 19 - .../Physics/RemotePhysicsBodyInitializer.cs | 39 -- .../Physics/RemoteTeleportController.cs | 10 +- .../Physics/RemoteTeleportPlacement.cs | 2 +- .../DatLiveEntityProjectionMaterializer.cs | 1 - .../Rendering/DevToolsRuntimeSources.cs | 4 +- .../Rendering/WorldRenderFrameBuilder.cs | 4 +- .../WorldSceneDiagnosticsController.cs | 4 +- .../Rendering/WorldSceneRuntimeSources.cs | 4 +- .../Runtime/CurrentGameRuntimeAdapter.cs | 2 - .../Runtime/CurrentGameRuntimeViewAdapter.cs | 14 +- .../LocalPlayerTeleportController.cs | 4 +- .../Streaming/StreamingFrameController.cs | 4 +- src/AcDream.App/World/LiveEntityRuntime.cs | 115 ++---- .../World/LiveEntityRuntimeViews.cs | 33 -- src/AcDream.Core/Physics/PhysicsEngine.cs | 21 + .../Physics/ShadowObjectRegistry.cs | 15 + .../Entities/RuntimeEntityObjectLifetime.cs | 63 ++- .../Entities/RuntimeEntityObjectViews.cs | 1 + src/AcDream.Runtime/GameRuntimeViews.cs | 2 + .../Physics/RuntimePhysicsState.cs | 82 +++- .../RuntimeSimulationOwnership.cs | 44 ++ .../Combat/LiveCombatAttackOperationsTests.cs | 2 +- tests/AcDream.App.Tests/GlobalUsings.cs | 14 - .../Input/MouseLookControllerTests.cs | 2 +- ...> RuntimeLocalPlayerMovementStateTests.cs} | 10 +- .../Physics/ProjectileControllerTests.cs | 43 +- .../Physics/RemoteTeleportControllerTests.cs | 6 +- .../Rendering/WorldRenderFrameBuilderTests.cs | 4 +- .../Runtime/CurrentGameRuntimeAdapterTests.cs | 1 - .../Runtime/RuntimePhysicsOwnershipTests.cs | 68 ++++ .../LocalPlayerTeleportControllerTests.cs | 4 +- .../StreamingFrameControllerTests.cs | 2 +- .../GameWindowLiveEntityCompositionTests.cs | 7 +- .../LiveEntityHydrationControllerTests.cs | 6 +- .../World/LiveEntityLifecycleStressTests.cs | 2 +- .../LiveEntityPhysicsHostOwnershipTests.cs | 16 +- .../World/LiveEntityRuntimeTests.cs | 39 +- .../World/RuntimeEntityOwnershipTests.cs | 19 + .../RuntimeEntityObjectLifetimeTests.cs | 10 +- .../Gameplay/RuntimeGameplayOwnershipTests.cs | 49 +++ .../Physics/RuntimePhysicsStateTests.cs | 113 +++++- .../RuntimeSimulationFixtureHostTests.cs | 384 ++++++++++++++++++ 57 files changed, 1013 insertions(+), 410 deletions(-) delete mode 100644 src/AcDream.App/Physics/RemotePhysicsBodyInitializer.cs create mode 100644 src/AcDream.Runtime/RuntimeSimulationOwnership.cs rename tests/AcDream.App.Tests/Input/{LocalPlayerControllerSlotTests.cs => RuntimeLocalPlayerMovementStateTests.cs} (89%) create mode 100644 tests/AcDream.Runtime.Tests/RuntimeSimulationFixtureHostTests.cs diff --git a/src/AcDream.App/Combat/LiveCombatAttackOperations.cs b/src/AcDream.App/Combat/LiveCombatAttackOperations.cs index 26e69ed6..1fe592f8 100644 --- a/src/AcDream.App/Combat/LiveCombatAttackOperations.cs +++ b/src/AcDream.App/Combat/LiveCombatAttackOperations.cs @@ -122,7 +122,7 @@ internal sealed class LiveCombatAttackOperations private readonly CombatState _combat; private readonly ICombatAttackTargetSource _targets; private readonly ICombatGameplaySettingsSource _settings; - private readonly ILocalPlayerControllerSource _player; + private readonly IRuntimeLocalPlayerControllerSource _player; private readonly LocalPlayerOutboundController _outbound; private readonly ILiveInWorldSource _inWorld; private readonly ILiveWorldSessionSource _session; @@ -132,7 +132,7 @@ internal sealed class LiveCombatAttackOperations CombatState combat, ICombatAttackTargetSource targets, ICombatGameplaySettingsSource settings, - ILocalPlayerControllerSource player, + IRuntimeLocalPlayerControllerSource player, LocalPlayerOutboundController outbound, ILiveInWorldSource inWorld, ILiveWorldSessionSource session, diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index bef3d3ed..337b54df 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -33,7 +33,7 @@ internal sealed record FrameRootDependencies( LocalPlayerModeState PlayerMode, LocalPlayerIdentityState PlayerIdentity, ChaseCameraInputState ChaseCameraInput, - LocalPlayerControllerSlot PlayerController, + RuntimeLocalPlayerMovementState PlayerController, LiveWorldOriginState WorldOrigin, ParticleVisibilityController ParticleVisibility, EntityEffectPoseRegistry EffectPoses, diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs index 822a1bf5..e30f2ef9 100644 --- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs +++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs @@ -56,7 +56,7 @@ internal sealed record InteractionRetainedUiDependencies( BufferedUiRegistry? UiRegistry, LiveCombatModeCommandSlot CombatModeCommands, ILocalPlayerIdentitySource PlayerIdentity, - ILocalPlayerControllerSource PlayerController, + IRuntimeLocalPlayerControllerSource PlayerController, ILocalPlayerModeSource PlayerMode, Func SelectionCameraFactory, diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index ca2f7c1e..ca59e8c8 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -60,7 +60,7 @@ internal sealed record LivePresentationDependencies( CellVisibility CellVisibility, LiveWorldOriginState WorldOrigin, LocalPlayerIdentityState PlayerIdentity, - LocalPlayerControllerSlot PlayerController, + RuntimeLocalPlayerMovementState PlayerController, PointerPositionState PointerPosition, PlayerApproachCompletionState PlayerApproachCompletions, GameRenderResourceLifetime RenderResourceLifetime, diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs index f948c4ce..daf3cea3 100644 --- a/src/AcDream.App/Composition/SessionPlayerComposition.cs +++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs @@ -60,7 +60,7 @@ internal sealed record SessionPlayerDependencies( RetailInboundEventDispatcher InboundEntityEvents, DeferredLiveEntityMotionRuntimeBindings MotionBindings, LocalPlayerIdentityState PlayerIdentity, - LocalPlayerControllerSlot PlayerController, + RuntimeLocalPlayerMovementState PlayerController, LocalPlayerPhysicsHostSlot PlayerHost, LocalPlayerModeState PlayerMode, ChaseCameraInputState ChaseCameraInput, @@ -507,7 +507,6 @@ internal sealed class SessionPlayerCompositionPhase live.ProjectileController, live.RemoteTeleport, d.AnimatedEntities, - new LiveEntityRemoteMotionRuntimeView(d.RuntimeSlot), d.RemoteMovementObservations, d.RemotePhysicsUpdater, d.RemoteInboundMotion, @@ -874,7 +873,6 @@ internal sealed class SessionPlayerCompositionPhase sessionHost, liveSessionCommands, d.PlayerIdentity, - live.LiveEntities, d.EntityObjects, d.Inventory, d.Character, diff --git a/src/AcDream.App/Diagnostics/RuntimeDiagnosticCommandController.cs b/src/AcDream.App/Diagnostics/RuntimeDiagnosticCommandController.cs index 56b941f4..4307e166 100644 --- a/src/AcDream.App/Diagnostics/RuntimeDiagnosticCommandController.cs +++ b/src/AcDream.App/Diagnostics/RuntimeDiagnosticCommandController.cs @@ -162,7 +162,7 @@ internal interface INearbyWorldDiagnosticSource internal sealed class RuntimeNearbyWorldDiagnosticSource : INearbyWorldDiagnosticSource { private readonly ILocalPlayerModeSource _mode; - private readonly ILocalPlayerControllerSource _player; + private readonly IRuntimeLocalPlayerControllerSource _player; private readonly CameraController _camera; private readonly LiveWorldOriginState _origin; private readonly GpuWorldState _world; @@ -170,7 +170,7 @@ internal sealed class RuntimeNearbyWorldDiagnosticSource : INearbyWorldDiagnosti public RuntimeNearbyWorldDiagnosticSource( ILocalPlayerModeSource mode, - ILocalPlayerControllerSource player, + IRuntimeLocalPlayerControllerSource player, CameraController camera, LiveWorldOriginState origin, GpuWorldState world, diff --git a/src/AcDream.App/GlobalUsings.cs b/src/AcDream.App/GlobalUsings.cs index d6122f76..5213b6cc 100644 --- a/src/AcDream.App/GlobalUsings.cs +++ b/src/AcDream.App/GlobalUsings.cs @@ -1,18 +1,4 @@ global using AcDream.Runtime.Gameplay; global using AcDream.Runtime.Physics; -global using ILiveEntityRemoteMotionRuntime = - AcDream.Runtime.Physics.IRuntimeRemoteMotion; -global using ILiveEntityPhysicsHostConsumer = - AcDream.Runtime.Physics.IRuntimePhysicsHostConsumer; -global using ILiveEntityCanonicalRuntimeConsumer = - AcDream.Runtime.Physics.IRuntimeCanonicalPhysicsConsumer; -global using ILiveEntityCanonicalCellConsumer = - AcDream.Runtime.Physics.IRuntimeCanonicalCellConsumer; -global using ILiveEntityRemotePlacementRuntime = - AcDream.Runtime.Physics.IRuntimeRemotePlacement; -global using LocalPlayerControllerSlot = - AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState; -global using ILocalPlayerControllerSource = - AcDream.Runtime.Gameplay.IRuntimeLocalPlayerControllerSource; global using ILocalPlayerMotionSource = AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource; diff --git a/src/AcDream.App/Input/LocalPlayerFrameRuntime.cs b/src/AcDream.App/Input/LocalPlayerFrameRuntime.cs index f659ca88..da3f4645 100644 --- a/src/AcDream.App/Input/LocalPlayerFrameRuntime.cs +++ b/src/AcDream.App/Input/LocalPlayerFrameRuntime.cs @@ -35,7 +35,7 @@ internal sealed class LiveLocalPlayerFrameRuntime : ILocalPlayerFrameRuntime { private readonly CameraController _camera; private readonly ILocalPlayerModeSource _mode; - private readonly ILocalPlayerControllerSource _controller; + private readonly IRuntimeLocalPlayerControllerSource _controller; private readonly IChaseCameraSource _chase; private readonly DispatcherMovementInputSource _input; private readonly IInputCaptureSource _capture; @@ -49,7 +49,7 @@ internal sealed class LiveLocalPlayerFrameRuntime : ILocalPlayerFrameRuntime public LiveLocalPlayerFrameRuntime( CameraController camera, ILocalPlayerModeSource mode, - ILocalPlayerControllerSource controller, + IRuntimeLocalPlayerControllerSource controller, IChaseCameraSource chase, DispatcherMovementInputSource input, IInputCaptureSource capture, diff --git a/src/AcDream.App/Input/MouseLookController.cs b/src/AcDream.App/Input/MouseLookController.cs index c8966f67..9e5bbe92 100644 --- a/src/AcDream.App/Input/MouseLookController.cs +++ b/src/AcDream.App/Input/MouseLookController.cs @@ -96,7 +96,7 @@ internal sealed class MouseLookController : IMouseLookInputFrameController private readonly IMouseSource _mouseSource; private readonly IPointerPositionSource _pointer; private readonly ILocalPlayerModeSource _playerMode; - private readonly ILocalPlayerControllerSource _playerController; + private readonly IRuntimeLocalPlayerControllerSource _playerController; private readonly CameraController _camera; private readonly ChaseCameraInputState _chase; private readonly IMovementInputSource _movementInput; @@ -111,7 +111,7 @@ internal sealed class MouseLookController : IMouseLookInputFrameController IMouseSource mouseSource, IPointerPositionSource pointer, ILocalPlayerModeSource playerMode, - ILocalPlayerControllerSource playerController, + IRuntimeLocalPlayerControllerSource playerController, CameraController camera, ChaseCameraInputState chase, IMovementInputSource movementInput, diff --git a/src/AcDream.App/Input/MovementTruthDiagnosticController.cs b/src/AcDream.App/Input/MovementTruthDiagnosticController.cs index 69aa8b75..67767d8f 100644 --- a/src/AcDream.App/Input/MovementTruthDiagnosticController.cs +++ b/src/AcDream.App/Input/MovementTruthDiagnosticController.cs @@ -7,7 +7,7 @@ internal sealed class MovementTruthDiagnosticController : IMovementTruthDiagnosticSink { private readonly bool _enabled; - private readonly ILocalPlayerControllerSource _player; + private readonly IRuntimeLocalPlayerControllerSource _player; private readonly ILocalPlayerIdentitySource _identity; private MovementTruthOutbound? _lastOutbound; @@ -24,7 +24,7 @@ internal sealed class MovementTruthDiagnosticController public MovementTruthDiagnosticController( bool enabled, - ILocalPlayerControllerSource player, + IRuntimeLocalPlayerControllerSource player, ILocalPlayerIdentitySource identity) { _enabled = enabled; diff --git a/src/AcDream.App/Input/PlayerModeController.cs b/src/AcDream.App/Input/PlayerModeController.cs index bc9b9ba2..81b9057c 100644 --- a/src/AcDream.App/Input/PlayerModeController.cs +++ b/src/AcDream.App/Input/PlayerModeController.cs @@ -23,7 +23,7 @@ internal sealed class PlayerModeController : IDevToolsPlayerModeTarget { private readonly LocalPlayerModeState _mode; - private readonly LocalPlayerControllerSlot _controllerSlot; + private readonly RuntimeLocalPlayerMovementState _controllerSlot; private readonly LocalPlayerPhysicsHostSlot _hostSlot; private readonly ChaseCameraInputState _chase; private readonly CameraController _camera; @@ -49,7 +49,7 @@ internal sealed class PlayerModeController : public PlayerModeController( LocalPlayerModeState mode, - LocalPlayerControllerSlot controllerSlot, + RuntimeLocalPlayerMovementState controllerSlot, LocalPlayerPhysicsHostSlot hostSlot, ChaseCameraInputState chase, CameraController camera, diff --git a/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs index 74587185..bacaabc3 100644 --- a/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs +++ b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs @@ -35,7 +35,7 @@ namespace AcDream.App.Net; internal sealed record LiveSessionPlayerRuntime( LocalPlayerIdentityState Identity, - LocalPlayerControllerSlot Controller, + RuntimeLocalPlayerMovementState Controller, LiveWorldOriginState WorldOrigin); internal sealed record LiveSessionDomainRuntime( diff --git a/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs b/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs index 78618fe4..ecf274c3 100644 --- a/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs +++ b/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs @@ -41,7 +41,6 @@ internal sealed class LiveEntityNetworkUpdateController private readonly ProjectileController _projectileController; private readonly RemoteTeleportController _remoteTeleportController; private readonly LiveEntityAnimationRuntimeView _animatedEntities; - private readonly LiveEntityRemoteMotionRuntimeView _remoteDeadReckon; private readonly RemoteMovementObservationTracker _remoteMovementObservations; private readonly RemotePhysicsUpdater _remotePhysicsUpdater; private readonly RemoteInboundMotionDispatcher _remoteInboundMotion; @@ -53,7 +52,7 @@ internal sealed class LiveEntityNetworkUpdateController private readonly LiveWorldOriginState _origin; private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink _localPlayerTeleport; - private readonly ILocalPlayerControllerSource _playerControllerSource; + private readonly IRuntimeLocalPlayerControllerSource _playerControllerSource; private readonly LocalPlayerOutboundController _localPlayerOutbound; private readonly ILocalPlayerPhysicsHostSource _playerHostSource; private readonly ILocalPlayerIdentitySource _playerIdentity; @@ -83,7 +82,6 @@ internal sealed class LiveEntityNetworkUpdateController ProjectileController projectileController, RemoteTeleportController remoteTeleportController, LiveEntityAnimationRuntimeView animatedEntities, - LiveEntityRemoteMotionRuntimeView remoteDeadReckon, RemoteMovementObservationTracker remoteMovementObservations, RemotePhysicsUpdater remotePhysicsUpdater, RemoteInboundMotionDispatcher remoteInboundMotion, @@ -94,7 +92,7 @@ internal sealed class LiveEntityNetworkUpdateController RuntimeCombatTargetState? combatTargetController, LiveWorldOriginState origin, AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport, - ILocalPlayerControllerSource playerControllerSource, + IRuntimeLocalPlayerControllerSource playerControllerSource, LocalPlayerOutboundController localPlayerOutbound, ILocalPlayerPhysicsHostSource playerHostSource, ILocalPlayerIdentitySource playerIdentity, @@ -113,7 +111,6 @@ internal sealed class LiveEntityNetworkUpdateController _projectileController = projectileController ?? throw new ArgumentNullException(nameof(projectileController)); _remoteTeleportController = remoteTeleportController ?? throw new ArgumentNullException(nameof(remoteTeleportController)); _animatedEntities = animatedEntities ?? throw new ArgumentNullException(nameof(animatedEntities)); - _remoteDeadReckon = remoteDeadReckon ?? throw new ArgumentNullException(nameof(remoteDeadReckon)); _remoteMovementObservations = remoteMovementObservations ?? throw new ArgumentNullException(nameof(remoteMovementObservations)); _remotePhysicsUpdater = remotePhysicsUpdater ?? throw new ArgumentNullException(nameof(remotePhysicsUpdater)); _remoteInboundMotion = remoteInboundMotion ?? throw new ArgumentNullException(nameof(remoteInboundMotion)); @@ -151,7 +148,10 @@ internal sealed class LiveEntityNetworkUpdateController uint localEntityId, Func isCurrent) { - _remoteDeadReckon.TryGetValue(serverGuid, out RemoteMotion? remote); + _liveEntities.TryGetRemoteMotionRuntime( + serverGuid, + out IRuntimeRemoteMotion? remoteRuntime); + RemoteMotion? remote = remoteRuntime as RemoteMotion; EntityPhysicsHost? host = _liveEntities.TryGetPhysicsHost(serverGuid, out var registered) ? registered as EntityPhysicsHost @@ -570,7 +570,10 @@ internal sealed class LiveEntityNetworkUpdateController _remoteMovementObservations[motionKey] = (prev.Pos, refreshedTime); } - if (_remoteDeadReckon.TryGetValue(update.Guid, out var dr)) + if (_liveEntities.TryGetRemoteMotionRuntime( + update.Guid, + out IRuntimeRemoteMotion? remoteRuntime) + && remoteRuntime is RemoteMotion dr) dr.LastServerPosTime = (refreshedTime - System.DateTime.UnixEpoch).TotalSeconds; } @@ -635,12 +638,15 @@ internal sealed class LiveEntityNetworkUpdateController if (!IsCurrentOwner()) return default; - if (!_remoteDeadReckon.TryGetValue(update.Guid, out var remote)) + if (!_liveEntities.TryGetRemoteMotionRuntime( + update.Guid, + out IRuntimeRemoteMotion? remoteRuntime) + || remoteRuntime is not RemoteMotion remote) { - remote = CreateRemoteMotion(update.Guid); + remote = _liveEntities.GetOrCreateRemoteMotionRuntime( + update.Guid); remote.Body.Orientation = entity.Rotation; remote.Body.Position = entity.Position; - _remoteDeadReckon[update.Guid] = remote; } if (!IsCurrentOwner(remote)) return default; @@ -719,43 +725,6 @@ internal sealed class LiveEntityNetworkUpdateController && runtime.IsCurrentSpatialRemoteMotion(record, remote); } - /// - /// Creates the optional MovementManager companion for one live object. - /// A retained projectile or static-animation owner contributes the - /// already-owned CPhysicsObj body; an ordinary remote gets local storage. - /// - private RemoteMotion CreateRemoteMotion(uint serverGuid) - { - LiveEntityRecord? record = null; - if (_liveEntities is { } liveEntities - && liveEntities.TryGetRecord(serverGuid, out var currentRecord)) - { - record = currentRecord; - } - - RemoteMotion remote; - if (record?.PhysicsBody is { } canonicalBody) - { - // Retail has one CPhysicsObj. A projectile or Physics-Static - // animation workset can create it before the MovementManager; - // adopt its current frame/vectors without replaying CreateObject. - remote = new RemoteMotion(canonicalBody); - } - else - { - remote = new RemoteMotion(); - if (record is not null) - AcDream.App.Physics.RemotePhysicsBodyInitializer.Initialize( - remote.Body, - record); - } - - RemoteMotion incarnation = remote; - remote.Movement.ActivatePhysicsObject = () => - _liveEntities?.TryActivateOrdinaryObject(serverGuid, incarnation); - return remote; - } - /// /// K-fix9 (2026-04-26): handle 0xF74E VectorUpdate from remote jumps. /// The payload seeds the world-space launch velocity and angular velocity. @@ -822,7 +791,13 @@ internal sealed class LiveEntityNetworkUpdateController if (!_liveEntities.ContainsWorldEntity(update.Guid)) return; if (update.Guid == _playerServerGuid) return; // local jump uses our own physics - if (!_remoteDeadReckon.TryGetValue(update.Guid, out var rm)) return; + if (!_liveEntities.TryGetRemoteMotionRuntime( + update.Guid, + out IRuntimeRemoteMotion? remoteRuntime) + || remoteRuntime is not RemoteMotion rm) + { + return; + } LiveEntityRecord remoteRecord = acceptedVectorRecord; // World-space velocity. Apply directly to the body — the per-tick @@ -1249,10 +1224,14 @@ internal sealed class LiveEntityNetworkUpdateController // small. When HasVelocity is set, we also seed PhysicsBody // velocity (matches retail's set_velocity call in the same // dispatcher). - if (!_remoteDeadReckon.TryGetValue(update.Guid, out var rmState)) + if (!_liveEntities.TryGetRemoteMotionRuntime( + update.Guid, + out IRuntimeRemoteMotion? remoteRuntime) + || remoteRuntime is not RemoteMotion rmState) { - rmState = CreateRemoteMotion(update.Guid); - _remoteDeadReckon[update.Guid] = rmState; + rmState = + _liveEntities.GetOrCreateRemoteMotionRuntime( + update.Guid); // Hard-snap orientation on first spawn so the per-tick // slerp doesn't visibly rotate from Identity to truth. rmState.Body.Orientation = rot; diff --git a/src/AcDream.App/Physics/LiveEntityShadowPublisher.cs b/src/AcDream.App/Physics/LiveEntityShadowPublisher.cs index 6bd6cbfc..58eaea19 100644 --- a/src/AcDream.App/Physics/LiveEntityShadowPublisher.cs +++ b/src/AcDream.App/Physics/LiveEntityShadowPublisher.cs @@ -14,7 +14,7 @@ internal static class LiveEntityShadowPublisher LiveEntityRuntime runtime, LiveEntityRecord record, WorldEntity entity, - ILiveEntityRemoteMotionRuntime remote, + IRuntimeRemoteMotion remote, ulong positionAuthorityVersion, Action publish) { @@ -47,7 +47,7 @@ internal static class LiveEntityShadowPublisher LiveEntityRuntime runtime, LiveEntityRecord record, WorldEntity entity, - ILiveEntityRemoteMotionRuntime remote, + IRuntimeRemoteMotion remote, ulong positionAuthorityVersion) => (record.FinalPhysicsState & PhysicsStateFlags.Hidden) == 0 && ReferenceEquals(record.WorldEntity, entity) diff --git a/src/AcDream.App/Physics/ProjectileController.cs b/src/AcDream.App/Physics/ProjectileController.cs index e7a836ea..b083e00d 100644 --- a/src/AcDream.App/Physics/ProjectileController.cs +++ b/src/AcDream.App/Physics/ProjectileController.cs @@ -246,7 +246,6 @@ internal sealed class ProjectileController { var created = new PhysicsBody { - State = record.FinalPhysicsState, Friction = NormalizeFriction( physics.Friction ?? record.Snapshot.Friction), Elasticity = NormalizeElasticity( @@ -254,8 +253,6 @@ internal sealed class ProjectileController Orientation = entity.Rotation, LastUpdateTime = currentTime, }; - SetVelocity(created, velocity, currentTime); - created.Omega = omega; created.SnapToCell( wirePosition.LandblockId, entity.Position, @@ -919,22 +916,6 @@ internal sealed class ProjectileController private static void Deactivate(PhysicsBody body) => body.TransientState &= ~TransientStateFlags.Active; - private static void SetVelocity( - PhysicsBody body, - Vector3 velocity, - double currentTime) - { - bool wasActive = (body.TransientState & TransientStateFlags.Active) != 0; - body.set_velocity(velocity); - if ((body.State & PhysicsStateFlags.Static) != 0) - { - Deactivate(body); - return; - } - if (!wasActive && (body.State & PhysicsStateFlags.Static) == 0) - body.LastUpdateTime = currentTime; - } - private static float NormalizeFriction(float? value) => value is >= 0f and <= 1f && float.IsFinite(value.Value) ? value.Value diff --git a/src/AcDream.App/Physics/RemotePhysicsBodyInitializer.cs b/src/AcDream.App/Physics/RemotePhysicsBodyInitializer.cs deleted file mode 100644 index 5f8cbd07..00000000 --- a/src/AcDream.App/Physics/RemotePhysicsBodyInitializer.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Numerics; -using AcDream.App.World; -using AcDream.Core.Physics; - -namespace AcDream.App.Physics; - -/// -/// Applies CreateObject physics vectors when the ordinary remote-motion body -/// is first constructed. This keeps initialization at object/body creation; -/// a later SetState classification change never replays retained vectors. -/// -/// -/// Retail CPhysicsObj::set_description 0x00514F40 installs the -/// unpacked velocity/omega during object initialization. Retail -/// SmartBox::DoSetState 0x004520D0 only calls -/// CPhysicsObj::set_state and therefore preserves the current vectors. -/// -internal static class RemotePhysicsBodyInitializer -{ - internal static void Initialize(PhysicsBody body, LiveEntityRecord record) - { - ArgumentNullException.ThrowIfNull(body); - ArgumentNullException.ThrowIfNull(record); - - body.State = record.FinalPhysicsState; - if (record.Snapshot.Physics is not { } physics) - return; - - if (physics.Velocity is { } velocity && IsFinite(velocity)) - body.set_velocity(velocity); - if (physics.AngularVelocity is { } omega && IsFinite(omega)) - body.Omega = omega; - } - - private static bool IsFinite(Vector3 value) => - float.IsFinite(value.X) - && float.IsFinite(value.Y) - && float.IsFinite(value.Z); -} diff --git a/src/AcDream.App/Physics/RemoteTeleportController.cs b/src/AcDream.App/Physics/RemoteTeleportController.cs index 97e0015c..8edf981a 100644 --- a/src/AcDream.App/Physics/RemoteTeleportController.cs +++ b/src/AcDream.App/Physics/RemoteTeleportController.cs @@ -73,7 +73,7 @@ internal sealed class RemoteTeleportController : IDisposable LiveEntityRecord Record, ulong PositionAuthorityVersion, ulong VelocityAuthorityVersion, - ILiveEntityRemotePlacementRuntime Remote, + IRuntimeRemotePlacement Remote, PhysicsBody Body, WorldEntity Entity, Vector3 RequestedWorldPosition, @@ -110,7 +110,7 @@ internal sealed class RemoteTeleportController : IDisposable /// revalidation once admitted. /// internal Result TryApply( - ILiveEntityRemotePlacementRuntime remote, + IRuntimeRemotePlacement remote, WorldEntity entity, Vector3 requestedWorldPosition, uint requestedCellId, @@ -147,7 +147,7 @@ internal sealed class RemoteTeleportController : IDisposable LiveEntityRecord expectedRecord, ulong expectedPositionAuthorityVersion, ulong expectedVelocityAuthorityVersion, - ILiveEntityRemotePlacementRuntime remote, + IRuntimeRemotePlacement remote, WorldEntity entity, Vector3 requestedWorldPosition, uint requestedCellId, @@ -422,7 +422,7 @@ internal sealed class RemoteTeleportController : IDisposable _pending.Remove(key); return; } - if (record.RemoteMotionRuntime is not ILiveEntityRemotePlacementRuntime currentRemote + if (record.RemoteMotionRuntime is not IRuntimeRemotePlacement currentRemote || record.PhysicsBody is null || !ReferenceEquals(record.PhysicsBody, pending.Body) || !ReferenceEquals(currentRemote.Body, record.PhysicsBody)) @@ -467,7 +467,7 @@ internal sealed class RemoteTeleportController : IDisposable } private static RollbackPlacement CaptureRollback( - ILiveEntityRemotePlacementRuntime remote, + IRuntimeRemotePlacement remote, PhysicsBody body) { return new RollbackPlacement( diff --git a/src/AcDream.App/Physics/RemoteTeleportPlacement.cs b/src/AcDream.App/Physics/RemoteTeleportPlacement.cs index f2fcd759..3212116a 100644 --- a/src/AcDream.App/Physics/RemoteTeleportPlacement.cs +++ b/src/AcDream.App/Physics/RemoteTeleportPlacement.cs @@ -13,7 +13,7 @@ namespace AcDream.App.Physics; internal static class RemoteTeleportPlacement { internal static bool Apply( - ILiveEntityRemotePlacementRuntime remote, + IRuntimeRemotePlacement remote, PhysicsBody body, ResolveResult placement, Vector3 cellLocalPosition, diff --git a/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs b/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs index f4e6db7b..3624f934 100644 --- a/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs +++ b/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs @@ -1005,7 +1005,6 @@ internal sealed class DatLiveEntityProjectionMaterializer incarnation => { var created = new PhysicsBody { Orientation = entity.Rotation }; - RemotePhysicsBodyInitializer.Initialize(created, incarnation); created.SnapToCell( staticPosition.LandblockId, entity.Position, diff --git a/src/AcDream.App/Rendering/DevToolsRuntimeSources.cs b/src/AcDream.App/Rendering/DevToolsRuntimeSources.cs index f9624c7c..b8b8f11f 100644 --- a/src/AcDream.App/Rendering/DevToolsRuntimeSources.cs +++ b/src/AcDream.App/Rendering/DevToolsRuntimeSources.cs @@ -257,7 +257,7 @@ internal interface IDevToolsRuntimeFacts internal sealed class DevToolsRuntimeFacts : IDevToolsRuntimeFacts { private readonly ILocalPlayerModeSource _mode; - private readonly ILocalPlayerControllerSource _player; + private readonly IRuntimeLocalPlayerControllerSource _player; private readonly CameraController _camera; private readonly ICanonicalWorldEntityCountSource _world; private readonly LiveEntityAnimationRuntimeView _animations; @@ -273,7 +273,7 @@ internal sealed class DevToolsRuntimeFacts : IDevToolsRuntimeFacts public DevToolsRuntimeFacts( ILocalPlayerModeSource mode, - ILocalPlayerControllerSource player, + IRuntimeLocalPlayerControllerSource player, CameraController camera, ICanonicalWorldEntityCountSource world, LiveEntityAnimationRuntimeView animations, diff --git a/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs b/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs index 41916cd3..db29e10d 100644 --- a/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs +++ b/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs @@ -218,7 +218,7 @@ internal sealed class RuntimeWorldFrameRootSource : IWorldFrameRootSource private readonly CellVisibility _cells; private readonly ILocalPlayerModeSource _mode; private readonly IChaseCameraSource _chase; - private readonly ILocalPlayerControllerSource _player; + private readonly IRuntimeLocalPlayerControllerSource _player; private readonly LiveWorldOriginState _origin; public RuntimeWorldFrameRootSource( @@ -226,7 +226,7 @@ internal sealed class RuntimeWorldFrameRootSource : IWorldFrameRootSource CellVisibility cells, ILocalPlayerModeSource mode, IChaseCameraSource chase, - ILocalPlayerControllerSource player, + IRuntimeLocalPlayerControllerSource player, LiveWorldOriginState origin) { _physics = physics ?? throw new ArgumentNullException(nameof(physics)); diff --git a/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs b/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs index 28e1c978..6a6d31c2 100644 --- a/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs +++ b/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs @@ -60,7 +60,7 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics private readonly DebugLineRenderer? _lines; private readonly PhysicsEngine _physics; private readonly ILocalPlayerModeSource _mode; - private readonly ILocalPlayerControllerSource _player; + private readonly IRuntimeLocalPlayerControllerSource _player; private readonly DebugVmRenderFactsPublisher _debugVm; private readonly bool _debugVmConsumerActive; private int _debugDrawLogCount; @@ -72,7 +72,7 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics DebugLineRenderer? lines, PhysicsEngine physics, ILocalPlayerModeSource mode, - ILocalPlayerControllerSource player, + IRuntimeLocalPlayerControllerSource player, DebugVmRenderFactsPublisher debugVm, bool debugVmConsumerActive) { diff --git a/src/AcDream.App/Rendering/WorldSceneRuntimeSources.cs b/src/AcDream.App/Rendering/WorldSceneRuntimeSources.cs index 5a8fa341..e8615988 100644 --- a/src/AcDream.App/Rendering/WorldSceneRuntimeSources.cs +++ b/src/AcDream.App/Rendering/WorldSceneRuntimeSources.cs @@ -55,12 +55,12 @@ internal interface IWorldScenePViewDiagnosticSource internal sealed class RuntimeWorldScenePViewDiagnosticSource : IWorldScenePViewDiagnosticSource { - private readonly ILocalPlayerControllerSource _player; + private readonly IRuntimeLocalPlayerControllerSource _player; private readonly PhysicsEngine _physics; private readonly CellVisibility _visibility; public RuntimeWorldScenePViewDiagnosticSource( - ILocalPlayerControllerSource player, + IRuntimeLocalPlayerControllerSource player, PhysicsEngine physics, CellVisibility visibility) { diff --git a/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs b/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs index 68d506ec..cbe52b2f 100644 --- a/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs +++ b/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs @@ -32,7 +32,6 @@ internal sealed class CurrentGameRuntimeAdapter LiveSessionHost sessionHost, ICommandBus commands, LocalPlayerIdentityState playerIdentity, - LiveEntityRuntime entities, RuntimeEntityObjectLifetime entityObjects, RuntimeInventoryState inventory, RuntimeCharacterState character, @@ -46,7 +45,6 @@ internal sealed class CurrentGameRuntimeAdapter _view = new CurrentGameRuntimeViewAdapter( session, playerIdentity, - entities, entityObjects, inventory, character, diff --git a/src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs b/src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs index fc4e23db..4d46037f 100644 --- a/src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs +++ b/src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs @@ -1,7 +1,6 @@ using AcDream.App.Input; using AcDream.App.Net; using AcDream.App.Streaming; -using AcDream.App.World; using AcDream.Core.Chat; using AcDream.Core.Items; using AcDream.Runtime; @@ -19,8 +18,6 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView { private readonly LiveSessionController _session; private readonly LocalPlayerIdentityState _playerIdentity; - private readonly LiveEntityRuntime _entities; - private readonly ClientObjectTable _objects; private readonly IGameRuntimeClock _clock; private readonly IRuntimeEntityView _entityView; private readonly IRuntimeInventoryView _inventoryView; @@ -36,7 +33,6 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView public CurrentGameRuntimeViewAdapter( LiveSessionController session, LocalPlayerIdentityState playerIdentity, - LiveEntityRuntime entities, RuntimeEntityObjectLifetime entityObjects, RuntimeInventoryState inventory, RuntimeCharacterState character, @@ -49,9 +45,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView _session = session ?? throw new ArgumentNullException(nameof(session)); _playerIdentity = playerIdentity ?? throw new ArgumentNullException(nameof(playerIdentity)); - _entities = entities ?? throw new ArgumentNullException(nameof(entities)); ArgumentNullException.ThrowIfNull(entityObjects); - _objects = entityObjects.Objects; ArgumentNullException.ThrowIfNull(communication); _clock = clock ?? throw new ArgumentNullException(nameof(clock)); @@ -115,10 +109,10 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView Generation, Lifecycle.State, _clock.FrameNumber, - _entities.Count, - _entities.MaterializedCount, - _objects.ObjectCount, - _objects.ContainerCount, + _entityView.Count, + _entityView.MaterializedCount, + _inventoryView.ObjectCount, + _inventoryView.ContainerCount, _inventoryStateView.Snapshot, _characterView.Snapshot, _socialView.Snapshot, diff --git a/src/AcDream.App/Streaming/LocalPlayerTeleportController.cs b/src/AcDream.App/Streaming/LocalPlayerTeleportController.cs index cdb83c6c..e0b8175f 100644 --- a/src/AcDream.App/Streaming/LocalPlayerTeleportController.cs +++ b/src/AcDream.App/Streaming/LocalPlayerTeleportController.cs @@ -179,7 +179,7 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme private readonly PhysicsEngine _physics; private readonly LiveEntityRuntime _liveEntities; private readonly ILocalPlayerIdentitySource _identity; - private readonly ILocalPlayerControllerSource _controller; + private readonly IRuntimeLocalPlayerControllerSource _controller; private readonly ILocalPlayerPhysicsHostSource _host; private readonly ChaseCameraInputState _cameras; private readonly LiveWorldOriginState _origin; @@ -189,7 +189,7 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme PhysicsEngine physics, LiveEntityRuntime liveEntities, ILocalPlayerIdentitySource identity, - ILocalPlayerControllerSource controller, + IRuntimeLocalPlayerControllerSource controller, ILocalPlayerPhysicsHostSource host, ChaseCameraInputState cameras, LiveWorldOriginState origin, diff --git a/src/AcDream.App/Streaming/StreamingFrameController.cs b/src/AcDream.App/Streaming/StreamingFrameController.cs index d32525b2..94197b6c 100644 --- a/src/AcDream.App/Streaming/StreamingFrameController.cs +++ b/src/AcDream.App/Streaming/StreamingFrameController.cs @@ -106,7 +106,7 @@ internal sealed class StreamingFrameController : IStreamingFramePhase private readonly bool _liveModeEnabled; private readonly ILocalPlayerModeSource _playerMode; - private readonly ILocalPlayerControllerSource _playerController; + private readonly IRuntimeLocalPlayerControllerSource _playerController; private readonly ILiveInWorldSource _session; private readonly LiveWorldOriginState _origin; private readonly ILocalPlayerLandblockSource _playerLandblock; @@ -119,7 +119,7 @@ internal sealed class StreamingFrameController : IStreamingFramePhase public StreamingFrameController( bool liveModeEnabled, ILocalPlayerModeSource playerMode, - ILocalPlayerControllerSource playerController, + IRuntimeLocalPlayerControllerSource playerController, ILiveInWorldSource session, LiveWorldOriginState origin, ILocalPlayerLandblockSource playerLandblock, diff --git a/src/AcDream.App/World/LiveEntityRuntime.cs b/src/AcDream.App/World/LiveEntityRuntime.cs index 9c74d95b..a15f0134 100644 --- a/src/AcDream.App/World/LiveEntityRuntime.cs +++ b/src/AcDream.App/World/LiveEntityRuntime.cs @@ -223,33 +223,27 @@ public sealed class LiveEntityRecord public PhysicsBody? PhysicsBody { get => Canonical.PhysicsBody; - internal set => _directory.SetPhysicsBody(Canonical, value); } internal bool PhysicsBodyAcquisitionInProgress { get => Canonical.PhysicsBodyAcquisitionInProgress; - set => _directory.SetPhysicsBodyAcquisitionInProgress(Canonical, value); } public ILiveEntityAnimationRuntime? AnimationRuntime { get; internal set; } - public ILiveEntityRemoteMotionRuntime? RemoteMotionRuntime + public IRuntimeRemoteMotion? RemoteMotionRuntime { get => Canonical.RemoteMotion; - internal set => _directory.SetRemoteMotion(Canonical, value); } internal bool RemoteMotionBindingInProgress { get => Canonical.RemoteMotionBindingInProgress; - set => _directory.SetRemoteMotionBindingInProgress(Canonical, value); } public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost { get => Canonical.PhysicsHost; - internal set => _directory.SetPhysicsHost(Canonical, value); } internal bool RequiresRemotePlacementRuntime { get => Canonical.RequiresRemotePlacementRuntime; - set => _directory.SetRequiresRemotePlacementRuntime(Canonical, value); } public IRuntimeProjectile? ProjectileRuntime => Canonical.Projectile; public ILiveEntityEffectProfile? EffectProfile { get; internal set; } @@ -1389,7 +1383,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource ProjectionIsCurrent); } - public void SetRemoteMotionRuntime(uint serverGuid, ILiveEntityRemoteMotionRuntime runtime) + internal void SetRemoteMotionRuntime(uint serverGuid, IRuntimeRemoteMotion runtime) { ArgumentNullException.ThrowIfNull(runtime); if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)) @@ -1442,7 +1436,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource public bool TryGetRemoteMotionRuntime( uint serverGuid, - out ILiveEntityRemoteMotionRuntime runtime) + out IRuntimeRemoteMotion runtime) { if (_projections.TryGetCurrent( serverGuid, @@ -1457,7 +1451,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource return false; } - public bool ClearRemoteMotionRuntime(uint serverGuid) + internal bool ClearRemoteMotionRuntime(uint serverGuid) { if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record) || record.RemoteMotionRuntime is null) @@ -1468,6 +1462,26 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource return true; } + internal RemoteMotion GetOrCreateRemoteMotionRuntime(uint serverGuid) + { + if (!_projections.TryGetCurrent( + serverGuid, + out LiveEntityRecord? record)) + { + throw new InvalidOperationException( + $"Cannot acquire remote motion before live entity 0x{serverGuid:X8} exists."); + } + + bool ProjectionIsCurrent() => + _projections.TryGetCurrent( + serverGuid, + out LiveEntityRecord? current) + && ReferenceEquals(current, record); + return _physics.GetOrCreateRemoteMotion( + record.Canonical, + ProjectionIsCurrent); + } + internal IRuntimeProjectile BindProjectileRuntime( uint serverGuid, PhysicsBody body, @@ -1874,27 +1888,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource destination.Add(key.LocalEntityId); } - /// - /// Host side of retail CPhysicsObj::set_active(1) for a movement - /// manager owned by this exact incarnation. Static is a required no-op. - /// - internal bool TryActivateOrdinaryObject( - uint serverGuid, - ILiveEntityRemoteMotionRuntime runtime) - { - ArgumentNullException.ThrowIfNull(runtime); - if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record) - || !ReferenceEquals(record.RemoteMotionRuntime, runtime) - || (record.FinalPhysicsState & PhysicsStateFlags.Static) != 0) - { - return false; - } - - record.ObjectClock.Activate(); - runtime.Body.TransientState |= TransientStateFlags.Active; - return true; - } - /// /// Commits retail CPhysicsObj::set_velocity (0x005113F0) to the /// canonical body for this exact incarnation. The body and the retained @@ -1941,41 +1934,19 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource { ArgumentNullException.ThrowIfNull(record); ArgumentNullException.ThrowIfNull(body); - if (!IsFinite(velocity) - || (angularVelocity is { } omega && !IsFinite(omega)) - || !double.IsFinite(currentTime) - || !_projections.TryGetCurrent( + bool ProjectionIsCurrent() => + _projections.TryGetCurrent( record.ServerGuid, out LiveEntityRecord? current) - || !ReferenceEquals(current, record) - || !ReferenceEquals(current.PhysicsBody, body)) - { - return false; - } - - bool wasBodyActive = - (body.TransientState & TransientStateFlags.Active) != 0; - body.set_velocity(velocity); - if ((current.FinalPhysicsState & PhysicsStateFlags.Static) != 0) - { - // PhysicsBody models set_velocity's leaf write and therefore sets - // Active unconditionally. Retail CPhysicsObj::set_active(1) is a - // no-op for Static, so restore the exact prior bit here. - if (!wasBodyActive) - body.TransientState &= ~TransientStateFlags.Active; - } - else - { - bool clockReactivated = current.ObjectClock.Activate(); - // PhysicsBody.LastUpdateTime remains only the compatibility clock - // for older absolute-time helpers; the record clock is canonical. - if (clockReactivated || !wasBodyActive) - body.LastUpdateTime = currentTime; - } - - if (angularVelocity is { } acceptedOmega) - body.Omega = acceptedOmega; - return true; + && ReferenceEquals(current, record) + && ReferenceEquals(current.PhysicsBody, body); + return _physics.TryCommitAuthoritativeVector( + record.Canonical, + body, + velocity, + angularVelocity, + currentTime, + ProjectionIsCurrent); } /// @@ -2007,7 +1978,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource internal bool IsCurrentSpatialRemoteMotion( LiveEntityRecord record, - ILiveEntityRemoteMotionRuntime runtime) => + IRuntimeRemoteMotion runtime) => IsCurrentRecord(record) && _physics.IsSpatialRemote(record.Canonical, runtime) && HasSpatialRuntimeProjection(record); @@ -2546,11 +2517,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource body.TransientState &= ~TransientStateFlags.Active; } - private static bool IsFinite(Vector3 value) => - float.IsFinite(value.X) - && float.IsFinite(value.Y) - && float.IsFinite(value.Z); - private void PublishProjectionVisibilityChanged(LiveEntityRecord record, bool visible) { Delegate[] subscribers = ProjectionVisibilityChanged?.GetInvocationList() @@ -2728,16 +2694,11 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource } if (record.WorldEntity is not null) - { _ = RequireProjectionKey(record); - _directory.ReleaseLocalId(record.Canonical); - } + _entityObjects.CompleteProjectionRetirement( + record.Canonical); record.AnimationRuntime = null; - record.RemoteMotionRuntime = null; - record.PhysicsHost = null; - record.RequiresRemotePlacementRuntime = false; - record.PhysicsBody = null; record.EffectProfile = null; record.IsSpatiallyProjected = false; record.IsSpatiallyVisible = false; diff --git a/src/AcDream.App/World/LiveEntityRuntimeViews.cs b/src/AcDream.App/World/LiveEntityRuntimeViews.cs index 8e24c308..79b9bd70 100644 --- a/src/AcDream.App/World/LiveEntityRuntimeViews.cs +++ b/src/AcDream.App/World/LiveEntityRuntimeViews.cs @@ -133,36 +133,3 @@ public sealed class LiveEntityAnimationRuntimeView throw new NotSupportedException(); } } - -/// Storage-free typed view over remote-motion components. -public sealed class LiveEntityRemoteMotionRuntimeView - where TRemoteMotion : class, ILiveEntityRemoteMotionRuntime -{ - private readonly ILiveEntityRuntimeSource _runtime; - - internal LiveEntityRemoteMotionRuntimeView(ILiveEntityRuntimeSource runtime) => - _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); - - public TRemoteMotion this[uint serverGuid] - { - set => (_runtime.Current - ?? throw new InvalidOperationException("Live entity runtime is not initialized.")) - .SetRemoteMotionRuntime(serverGuid, value); - } - - public bool TryGetValue(uint serverGuid, out TRemoteMotion motion) - { - if (_runtime.Current?.TryGetRemoteMotionRuntime(serverGuid, out var found) == true - && found is TRemoteMotion typed) - { - motion = typed; - return true; - } - - motion = null!; - return false; - } - - public bool Remove(uint serverGuid) => - _runtime.Current?.ClearRemoteMotionRuntime(serverGuid) == true; -} diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 17e7becc..56348666 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -168,6 +168,27 @@ public sealed class PhysicsEngine DataCache?.CellGraph.RemoveLandblock(landblockId); } + /// + /// Releases every collision landblock and retained shadow registration + /// owned by this engine. Runtime calls this only at terminal disposal; + /// ordinary streaming still uses the typed per-landblock retirement path. + /// + public void Clear() + { + if (_landblocks.Count != 0) + { + var landblocks = new uint[_landblocks.Count]; + _landblocks.Keys.CopyTo(landblocks, 0); + foreach (uint landblockId in landblocks) + RemoveLandblock(landblockId); + } + + // Dynamic live registrations can remain intentionally suspended with + // no cell rows after their last landblock retires. Terminal engine + // ownership must retire those logical registrations as well. + ShadowObjects.Clear(); + } + /// /// Retire a Near landblock's indoor-cell and static-object collision layer /// while preserving its terrain surface and world offset for Far-tier use. diff --git a/src/AcDream.Core/Physics/ShadowObjectRegistry.cs b/src/AcDream.Core/Physics/ShadowObjectRegistry.cs index ecb3c08c..b511a81a 100644 --- a/src/AcDream.Core/Physics/ShadowObjectRegistry.cs +++ b/src/AcDream.Core/Physics/ShadowObjectRegistry.cs @@ -795,6 +795,21 @@ public sealed class ShadowObjectRegistry /// Suspended logical registrations awaiting spatial re-entry. public int SuspendedRegistrationCount => _suspendedEntities.Count; + /// + /// Retires the complete logical registry at terminal physics-engine + /// disposal, including suspended live registrations that own no cell row. + /// + public void Clear() + { + _cells.Clear(); + _entityToCells.Clear(); + _suspendedEntities.Clear(); + _withdrawnPrefixesByOwner.Clear(); + _entityShapes.Clear(); + _entityReg.Clear(); + _fallback = null; + } + /// /// Debug: enumerate every registered ShadowEntry (deduplicated across cells). /// Single-shape entities return one entry per shape; multi-part entities diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs index 8f6c6039..19b8dedd 100644 --- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs +++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs @@ -32,7 +32,29 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot( bool HasLastStreamDispatchFailure, int PendingDispatchCount, bool IsDispatching, - bool IsSessionClearInProgress); + bool IsSessionClearInProgress, + bool IsDisposed) +{ + public bool IsConverged => + IsDisposed + && ActiveEntityCount == 0 + && TeardownEntityCount == 0 + && ClaimedLocalIdCount == 0 + && AcceptedSnapshotCount == 0 + && UnresolvedParentRelationCount == 0 + && StagedParentRelationCount == 0 + && RecoveryParentRelationCount == 0 + && CommittedParentRelationCount == 0 + && ObjectCount == 0 + && ContainerCount == 0 + && ContainerProjectionCount == 0 + && EquipmentOwnerCount == 0 + && PendingMoveCount == 0 + && StreamSubscriberCount == 0 + && PendingDispatchCount == 0 + && !IsDispatching + && !IsSessionClearInProgress; +} /// /// One exact DeleteObject acceptance issued by a @@ -140,7 +162,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable Events.LastDispatchFailure is not null, Events.PendingDispatchCount, Events.IsDispatching, - _sessionClearInProgress); + _sessionClearInProgress, + _disposed); } public void BindEventContext( @@ -355,17 +378,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable Entities.RetainTeardown(canonical); try { - Physics.RemoveSpatialProjection(canonical); - Entities.SetRemoteMotion(canonical, null); - Entities.SetRemoteMotionBindingInProgress(canonical, false); - Entities.SetProjectile(canonical, null); - Entities.SetProjectileBindingInProgress(canonical, false); - Entities.SetRequiresRemotePlacementRuntime(canonical, false); - Entities.SetPhysicsHost(canonical, null); - Entities.SetPhysicsBody(canonical, null); - Entities.SetPhysicsBodyAcquisitionInProgress(canonical, false); - Entities.SetHasPartArray(canonical, false); - Entities.ReleaseLocalId(canonical); + CompleteProjectionRetirement(canonical); } finally { @@ -379,6 +392,30 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable } } + /// + /// Completes the presentation acknowledgement for one retained + /// incarnation. Runtime retires every presentation-independent component + /// and releases its local identity only after the graphical host has + /// finished using those exact components for ExitWorld teardown. + /// + internal void CompleteProjectionRetirement( + RuntimeEntityRecord canonical) + { + EnsureNotDisposed(); + ArgumentNullException.ThrowIfNull(canonical); + Physics.RemoveSpatialProjection(canonical); + Entities.SetRemoteMotion(canonical, null); + Entities.SetRemoteMotionBindingInProgress(canonical, false); + Entities.SetProjectile(canonical, null); + Entities.SetProjectileBindingInProgress(canonical, false); + Entities.SetRequiresRemotePlacementRuntime(canonical, false); + Entities.SetPhysicsHost(canonical, null); + Entities.SetPhysicsBody(canonical, null); + Entities.SetPhysicsBodyAcquisitionInProgress(canonical, false); + Entities.SetHasPartArray(canonical, false); + Entities.ReleaseLocalId(canonical); + } + /// /// Applies the retained-object half of an accepted CreateObject while the /// exact canonical incarnation and its integration version remain current. diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectViews.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectViews.cs index f6e9b2cc..26f27e23 100644 --- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectViews.cs +++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectViews.cs @@ -85,6 +85,7 @@ internal sealed class RuntimeEntityObjectViews : IRuntimeEntityView { public int Count => owner.Count; + public int MaterializedCount => owner.ClaimedLocalIdCount; public bool TryGet( uint serverGuid, diff --git a/src/AcDream.Runtime/GameRuntimeViews.cs b/src/AcDream.Runtime/GameRuntimeViews.cs index 8b3cf7c0..47511ce8 100644 --- a/src/AcDream.Runtime/GameRuntimeViews.cs +++ b/src/AcDream.Runtime/GameRuntimeViews.cs @@ -22,6 +22,8 @@ public interface IRuntimeEntityView { int Count { get; } + int MaterializedCount { get; } + bool TryGet(uint serverGuid, out RuntimeEntitySnapshot entity); /// diff --git a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs index 18dc4608..1cb12b18 100644 --- a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs +++ b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs @@ -12,7 +12,19 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot( int CollisionAdmissionCount, int CollisionGenerationCount, bool OwnsProductionDataCache, - bool IsDisposed); + bool IsDisposed) +{ + public bool IsConverged => + IsDisposed + && LandblockCount == 0 + && RetainedShadowRegistrationCount == 0 + && SpatialRootCount == 0 + && SpatialRemoteCount == 0 + && SpatialProjectileCount == 0 + && CollisionAdmissionCount == 0 + && CollisionGenerationCount == 0 + && OwnsProductionDataCache; +} public readonly record struct RuntimePhysicsCellCommit( RuntimeEntityRecord Record, @@ -437,6 +449,8 @@ public sealed class RuntimePhysicsState : IDisposable record, expectedPlacementContract || runtime is IRuntimeRemotePlacement); + if (expectedBody is null) + InitializeNewPhysicsBody(record, candidateBody); Entities.SetRemoteMotion(record, runtime); Entities.SetPhysicsBody(record, candidateBody); candidateBody.State = record.FinalPhysicsState; @@ -452,6 +466,52 @@ public sealed class RuntimePhysicsState : IDisposable } } + /// + /// Constructs the one built-in remote-motion component for an exact + /// canonical incarnation. A graphical host may attach animation and pose + /// sinks afterward, but cannot construct or replace simulation state. + /// + internal RemoteMotion GetOrCreateRemoteMotion( + RuntimeEntityRecord record, + Func? externalOwnerValid = null) + { + EnsureNotDisposed(); + ArgumentNullException.ThrowIfNull(record); + EnsureCurrent(record); + if (record.RemoteMotion is { } retained) + { + return retained as RemoteMotion + ?? throw new InvalidOperationException( + $"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} owns a non-production remote-motion component."); + } + + var created = new RemoteMotion(record.PhysicsBody); + created.Movement.ActivatePhysicsObject = () => + TryActivateOrdinaryObject(record, created); + SetRemoteMotion(record, created, externalOwnerValid); + return created; + } + + internal bool TryActivateOrdinaryObject( + RuntimeEntityRecord record, + IRuntimeRemoteMotion runtime, + Func? externalOwnerValid = null) + { + ArgumentNullException.ThrowIfNull(record); + ArgumentNullException.ThrowIfNull(runtime); + if (!Entities.IsCurrent(record) + || !ReferenceEquals(record.RemoteMotion, runtime) + || (record.FinalPhysicsState & PhysicsStateFlags.Static) != 0 + || !(externalOwnerValid?.Invoke() ?? true)) + { + return false; + } + + record.ObjectClock.Activate(); + runtime.Body.TransientState |= TransientStateFlags.Active; + return true; + } + /// /// Acquires the one retail CPhysicsObj body for an exact accepted /// object incarnation. Factories may cross a host boundary, so ownership @@ -499,6 +559,7 @@ public sealed class RuntimePhysicsState : IDisposable $"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} acquired two physics bodies within one incarnation."); } + InitializeNewPhysicsBody(record, candidate); Entities.SetPhysicsBody(record, candidate); candidate.State = record.FinalPhysicsState; SynchronizeBodyActiveState(record); @@ -905,6 +966,7 @@ public sealed class RuntimePhysicsState : IDisposable { if (_disposed) return; + Engine.Clear(); _spatialRemotes.Clear(); _spatialProjectiles.Clear(); _spatialRoots.Clear(); @@ -1039,4 +1101,22 @@ public sealed class RuntimePhysicsState : IDisposable else body.TransientState &= ~TransientStateFlags.Active; } + + /// + /// Retail CPhysicsObj::set_description (0x00514F40) installs the + /// accepted CreateObject velocity and omega once, when the incarnation's + /// CPhysicsObj is first acquired. Later SetState never replays them. + /// + private static void InitializeNewPhysicsBody( + RuntimeEntityRecord record, + PhysicsBody body) + { + body.State = record.FinalPhysicsState; + if (record.Snapshot.Physics is not { } physics) + return; + if (physics.Velocity is { } velocity && IsFinite(velocity)) + body.set_velocity(velocity); + if (physics.AngularVelocity is { } omega && IsFinite(omega)) + body.Omega = omega; + } } diff --git a/src/AcDream.Runtime/RuntimeSimulationOwnership.cs b/src/AcDream.Runtime/RuntimeSimulationOwnership.cs new file mode 100644 index 00000000..7e05e4c7 --- /dev/null +++ b/src/AcDream.Runtime/RuntimeSimulationOwnership.cs @@ -0,0 +1,44 @@ +using AcDream.Runtime.Entities; +using AcDream.Runtime.Gameplay; +using AcDream.Runtime.Physics; + +namespace AcDream.Runtime; + +/// +/// One allocation-free terminal ownership ledger for the complete J5 +/// presentation-independent simulation graph. It owns no mutable state. +/// Graphical and no-window hosts inspect the same canonical owners. +/// +public readonly record struct RuntimeSimulationOwnershipSnapshot( + RuntimeEntityObjectOwnershipSnapshot EntityObjects, + RuntimePhysicsOwnershipSnapshot Physics, + RuntimeGameplayOwnershipSnapshot Gameplay) +{ + public bool IsConverged => + EntityObjects.IsConverged + && Physics.IsConverged + && Gameplay.IsConverged; +} + +public static class RuntimeSimulationOwnership +{ + public static RuntimeSimulationOwnershipSnapshot Capture( + RuntimeEntityObjectLifetime entityObjects, + RuntimeInventoryState inventory, + RuntimeCharacterState character, + RuntimeCommunicationState communication, + RuntimeActionState actions, + RuntimeLocalPlayerMovementState movement) + { + ArgumentNullException.ThrowIfNull(entityObjects); + return new RuntimeSimulationOwnershipSnapshot( + entityObjects.CaptureOwnership(), + entityObjects.Physics.CaptureOwnership(), + RuntimeGameplayOwnership.Capture( + inventory, + character, + communication, + actions, + movement)); + } +} diff --git a/tests/AcDream.App.Tests/Combat/LiveCombatAttackOperationsTests.cs b/tests/AcDream.App.Tests/Combat/LiveCombatAttackOperationsTests.cs index a35b9ff6..12ca50ae 100644 --- a/tests/AcDream.App.Tests/Combat/LiveCombatAttackOperationsTests.cs +++ b/tests/AcDream.App.Tests/Combat/LiveCombatAttackOperationsTests.cs @@ -96,7 +96,7 @@ public sealed class LiveCombatAttackOperationsTests var combat = new CombatState(); var targets = new FakeTargets(); var settings = new FakeSettings(); - var player = new LocalPlayerControllerSlot(); + var player = new RuntimeLocalPlayerMovementState(); var live = new FakeLiveSource { IsInWorld = inWorld }; var feedback = new FakeFeedback(); var outbound = new LocalPlayerOutboundController( diff --git a/tests/AcDream.App.Tests/GlobalUsings.cs b/tests/AcDream.App.Tests/GlobalUsings.cs index d6122f76..5213b6cc 100644 --- a/tests/AcDream.App.Tests/GlobalUsings.cs +++ b/tests/AcDream.App.Tests/GlobalUsings.cs @@ -1,18 +1,4 @@ global using AcDream.Runtime.Gameplay; global using AcDream.Runtime.Physics; -global using ILiveEntityRemoteMotionRuntime = - AcDream.Runtime.Physics.IRuntimeRemoteMotion; -global using ILiveEntityPhysicsHostConsumer = - AcDream.Runtime.Physics.IRuntimePhysicsHostConsumer; -global using ILiveEntityCanonicalRuntimeConsumer = - AcDream.Runtime.Physics.IRuntimeCanonicalPhysicsConsumer; -global using ILiveEntityCanonicalCellConsumer = - AcDream.Runtime.Physics.IRuntimeCanonicalCellConsumer; -global using ILiveEntityRemotePlacementRuntime = - AcDream.Runtime.Physics.IRuntimeRemotePlacement; -global using LocalPlayerControllerSlot = - AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState; -global using ILocalPlayerControllerSource = - AcDream.Runtime.Gameplay.IRuntimeLocalPlayerControllerSource; global using ILocalPlayerMotionSource = AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource; diff --git a/tests/AcDream.App.Tests/Input/MouseLookControllerTests.cs b/tests/AcDream.App.Tests/Input/MouseLookControllerTests.cs index 6c6e2889..4d1702c7 100644 --- a/tests/AcDream.App.Tests/Input/MouseLookControllerTests.cs +++ b/tests/AcDream.App.Tests/Input/MouseLookControllerTests.cs @@ -201,7 +201,7 @@ public sealed class MouseLookControllerTests { PlayerMovementController player = CreatePlayer(); var mode = new LocalPlayerModeState(); - var slot = new LocalPlayerControllerSlot { Controller = player }; + var slot = new RuntimeLocalPlayerMovementState { Controller = player }; var camera = new CameraController(new OrbitCamera(), new FlyCamera()); var chase = new ChaseCameraInputState(); var mouse = new FakeMouse(); diff --git a/tests/AcDream.App.Tests/Input/LocalPlayerControllerSlotTests.cs b/tests/AcDream.App.Tests/Input/RuntimeLocalPlayerMovementStateTests.cs similarity index 89% rename from tests/AcDream.App.Tests/Input/LocalPlayerControllerSlotTests.cs rename to tests/AcDream.App.Tests/Input/RuntimeLocalPlayerMovementStateTests.cs index 6e891311..dff6b60d 100644 --- a/tests/AcDream.App.Tests/Input/LocalPlayerControllerSlotTests.cs +++ b/tests/AcDream.App.Tests/Input/RuntimeLocalPlayerMovementStateTests.cs @@ -3,12 +3,12 @@ using AcDream.Core.Physics; namespace AcDream.App.Tests.Input; -public sealed class LocalPlayerControllerSlotTests +public sealed class RuntimeLocalPlayerMovementStateTests { [Fact] public void MotionPreparation_RoutesCompletionWithoutPublishingController() { - var slot = new LocalPlayerControllerSlot(); + var slot = new RuntimeLocalPlayerMovementState(); ILocalPlayerMotionSource source = slot; var controller = new PlayerMovementController(new PhysicsEngine()); controller.Motion.AddToQueue(0, MotionCommand.Ready, 0); @@ -29,7 +29,7 @@ public sealed class LocalPlayerControllerSlotTests [Fact] public void MotionPreparation_HandsOffToCommittedControllerOnDispose() { - var slot = new LocalPlayerControllerSlot(); + var slot = new RuntimeLocalPlayerMovementState(); ILocalPlayerMotionSource source = slot; var controller = new PlayerMovementController(new PhysicsEngine()); @@ -45,7 +45,7 @@ public sealed class LocalPlayerControllerSlotTests [Fact] public void MotionPreparation_DrainsPriorAnimationBeforePublishingCandidate() { - var slot = new LocalPlayerControllerSlot(); + var slot = new RuntimeLocalPlayerMovementState(); ILocalPlayerMotionSource source = slot; var prior = new PlayerMovementController(new PhysicsEngine()); var candidate = new PlayerMovementController(new PhysicsEngine()); @@ -71,7 +71,7 @@ public sealed class LocalPlayerControllerSlotTests [Fact] public void MotionPreparation_RejectsConcurrentCandidate() { - var slot = new LocalPlayerControllerSlot(); + var slot = new RuntimeLocalPlayerMovementState(); var first = new PlayerMovementController(new PhysicsEngine()); var second = new PlayerMovementController(new PhysicsEngine()); diff --git a/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs b/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs index 1434b067..a3baee81 100644 --- a/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs +++ b/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs @@ -105,11 +105,10 @@ public sealed class ProjectileControllerTests LiveEntityRecord record = fixture.Spawn(instance: 1); WorldEntity entity = record.WorldEntity!; Assert.Null(record.AnimationRuntime); - var remote = new AcDream.Runtime.Physics.RemoteMotion(); - RemotePhysicsBodyInitializer.Initialize(remote.Body, record); + var remote = + fixture.Live.GetOrCreateRemoteMotionRuntime(Guid); remote.Body.Position = entity.Position; remote.Body.Orientation = entity.Rotation; - fixture.Live.SetRemoteMotionRuntime(Guid, remote); Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1)); Assert.Same(remote.Body, record.ProjectileRuntime!.Body); @@ -686,7 +685,9 @@ public sealed class ProjectileControllerTests LiveEntityRecord record = fixture.Spawn(instance: 1); Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0)); PhysicsBody body = record.PhysicsBody!; - fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.Runtime.Physics.RemoteMotion(body)); + Assert.Same( + body, + fixture.Live.GetOrCreateRemoteMotionRuntime(Guid).Body); record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; Assert.True(fixture.Controller.ApplyAuthoritativeState( record, record.FinalPhysicsState, 2.0, 1, 1)); @@ -710,7 +711,9 @@ public sealed class ProjectileControllerTests Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0)); PhysicsBody body = record.PhysicsBody!; Vector3 position = body.Position; - fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.Runtime.Physics.RemoteMotion(body)); + Assert.Same( + body, + fixture.Live.GetOrCreateRemoteMotionRuntime(Guid).Body); record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; Assert.True(fixture.Controller.ApplyAuthoritativeState( @@ -949,9 +952,8 @@ public sealed class ProjectileControllerTests var fixture = new Fixture(); LiveEntityRecord record = fixture.Spawn(instance: 1); record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; - var remote = new AcDream.Runtime.Physics.RemoteMotion(); - RemotePhysicsBodyInitializer.Initialize(remote.Body, record); - fixture.Live.SetRemoteMotionRuntime(Guid, remote); + var remote = + fixture.Live.GetOrCreateRemoteMotionRuntime(Guid); PhysicsStateFlags ordinaryState = PhysicsStateFlags.Gravity | PhysicsStateFlags.IgnoreCollisions; @@ -1001,9 +1003,8 @@ public sealed class ProjectileControllerTests var fixture = new Fixture(); LiveEntityRecord record = fixture.Spawn(instance: 1); record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; - var remote = new AcDream.Runtime.Physics.RemoteMotion(); - RemotePhysicsBodyInitializer.Initialize(remote.Body, record); - fixture.Live.SetRemoteMotionRuntime(Guid, remote); + var remote = + fixture.Live.GetOrCreateRemoteMotionRuntime(Guid); record.FinalPhysicsState = MissileState; Assert.True(fixture.Controller.ApplyAuthoritativeState( @@ -1073,15 +1074,14 @@ public sealed class ProjectileControllerTests seedCellId: startCell, isStatic: false); record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; - var remote = new AcDream.Runtime.Physics.RemoteMotion(); - RemotePhysicsBodyInitializer.Initialize(remote.Body, record); + var remote = + fixture.Live.GetOrCreateRemoteMotionRuntime(Guid); Assert.Equal(new Vector3(40f, 0f, 0f), remote.Body.Velocity); Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega); remote.Body.set_velocity(new Vector3(8f, 0f, 0f)); remote.Body.Omega = new Vector3(0f, 0f, 3f); remote.Body.Position = entity.Position; remote.Body.State = record.FinalPhysicsState; - fixture.Live.SetRemoteMotionRuntime(Guid, remote); PhysicsBody body = remote.Body; record.FinalPhysicsState = MissileState; @@ -1139,11 +1139,10 @@ public sealed class ProjectileControllerTests velocity: new Vector3(float.NaN, 0f, 0f)); WorldEntity entity = record.WorldEntity!; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; - var remote = new AcDream.Runtime.Physics.RemoteMotion(); - RemotePhysicsBodyInitializer.Initialize(remote.Body, record); + var remote = + fixture.Live.GetOrCreateRemoteMotionRuntime(Guid); remote.Body.Position = entity.Position; remote.Body.Orientation = entity.Rotation; - fixture.Live.SetRemoteMotionRuntime(Guid, remote); Assert.True(float.IsNaN(record.Snapshot.Physics!.Value.Velocity!.Value.X)); Assert.Equal(Vector3.Zero, remote.Body.Velocity); @@ -1165,12 +1164,11 @@ public sealed class ProjectileControllerTests LiveEntityRecord record = fixture.Spawn(instance: 1); WorldEntity entity = record.WorldEntity!; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; - var remote = new AcDream.Runtime.Physics.RemoteMotion(); - RemotePhysicsBodyInitializer.Initialize(remote.Body, record); + var remote = + fixture.Live.GetOrCreateRemoteMotionRuntime(Guid); remote.Body.Position = entity.Position; remote.Body.Orientation = entity.Rotation; remote.Body.Velocity = new Vector3(float.PositiveInfinity, 0f, 0f); - fixture.Live.SetRemoteMotionRuntime(Guid, remote); record.FinalPhysicsState = MissileState; Assert.True(fixture.Controller.ApplyAuthoritativeState( @@ -1189,8 +1187,9 @@ public sealed class ProjectileControllerTests LiveEntityRecord record = fixture.Spawn(instance: 1); Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0)); PhysicsBody body = record.PhysicsBody!; - var remote = new AcDream.Runtime.Physics.RemoteMotion(body); - fixture.Live.SetRemoteMotionRuntime(Guid, remote); + var remote = + fixture.Live.GetOrCreateRemoteMotionRuntime(Guid); + Assert.Same(body, remote.Body); fixture.Live.RebucketLiveEntity(Guid, CellB); Assert.Equal(CellB, remote.CellId); diff --git a/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs b/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs index abbcce91..ffd6cdfd 100644 --- a/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs +++ b/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs @@ -19,12 +19,12 @@ public sealed class RemoteTeleportControllerTests public void Unregister(WorldEntity entity) { } } - private sealed class BodyOnlyRemote(PhysicsBody body) : ILiveEntityRemoteMotionRuntime + private sealed class BodyOnlyRemote(PhysicsBody body) : IRuntimeRemoteMotion { public PhysicsBody Body { get; } = body; } - private sealed class MutablePlacementRemote(PhysicsBody body) : ILiveEntityRemotePlacementRuntime + private sealed class MutablePlacementRemote(PhysicsBody body) : IRuntimeRemotePlacement { private uint _cellId; private Func? _readCell; @@ -56,7 +56,7 @@ public sealed class RemoteTeleportControllerTests private sealed class AlternatingBodyRemote( PhysicsBody first, - PhysicsBody later) : ILiveEntityRemotePlacementRuntime + PhysicsBody later) : IRuntimeRemotePlacement { private int _reads; public PhysicsBody Body => _reads++ == 0 ? first : later; diff --git a/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs b/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs index 8c41edc0..6bf373bf 100644 --- a/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs +++ b/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs @@ -190,7 +190,7 @@ public sealed class WorldRenderFrameBuilderTests cells, mode, chase, - new LocalPlayerControllerSlot(), + new RuntimeLocalPlayerMovementState(), origin); var camera = new FlyCamera { Position = new Vector3(5f, 6f, 7f) }; WorldCameraFrame cameraFrame = CameraFrame(camera); @@ -227,7 +227,7 @@ public sealed class WorldRenderFrameBuilderTests new CellVisibility(), new LocalPlayerModeState(), new ChaseCameraInputState(), - new LocalPlayerControllerSlot(), + new RuntimeLocalPlayerMovementState(), new LiveWorldOriginState()); WorldCameraFrame camera = CameraFrame(new FlyCamera()); diff --git a/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs b/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs index f5fe514b..9d14f491 100644 --- a/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs +++ b/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs @@ -827,7 +827,6 @@ public sealed class CurrentGameRuntimeAdapterTests Host, Commands, Identity, - Entities, EntityObjects, InventoryState, Character, diff --git a/tests/AcDream.App.Tests/Runtime/RuntimePhysicsOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimePhysicsOwnershipTests.cs index b30e5d68..8d49d6b2 100644 --- a/tests/AcDream.App.Tests/Runtime/RuntimePhysicsOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Runtime/RuntimePhysicsOwnershipTests.cs @@ -69,6 +69,13 @@ public sealed class RuntimePhysicsOwnershipTests runtimeRoot, "Physics", "RuntimePhysicsState.cs")); + string app = string.Join( + "\n", + Directory.EnumerateFiles( + appRoot, + "*.cs", + SearchOption.AllDirectories) + .Select(File.ReadAllText)); Assert.DoesNotContain( "ResolveWithTransition(", @@ -131,6 +138,67 @@ public sealed class RuntimePhysicsOwnershipTests "public bool TryGetPhysicsHost(", runtimePhysics, StringComparison.Ordinal); + Assert.Contains( + "GetOrCreateRemoteMotion(", + runtimePhysics, + StringComparison.Ordinal); + Assert.Contains( + "_physics.TryCommitAuthoritativeVector(", + liveRuntime, + StringComparison.Ordinal); + Assert.DoesNotContain( + "TryActivateOrdinaryObject(", + liveRuntime, + StringComparison.Ordinal); + Assert.Contains( + "_entityObjects.CompleteProjectionRetirement(", + liveRuntime, + StringComparison.Ordinal); + Assert.DoesNotContain( + "record.RemoteMotionRuntime =", + liveRuntime, + StringComparison.Ordinal); + Assert.DoesNotContain( + "record.PhysicsBody =", + liveRuntime, + StringComparison.Ordinal); + Assert.DoesNotContain( + "new RemoteMotion(", + app, + StringComparison.Ordinal); + Assert.DoesNotContain( + "LiveEntityRemoteMotionRuntimeView", + app, + StringComparison.Ordinal); + Assert.DoesNotContain( + "ILiveEntityRemoteMotionRuntime", + app, + StringComparison.Ordinal); + Assert.DoesNotContain( + "ILiveEntityRemotePlacementRuntime", + app, + StringComparison.Ordinal); + Assert.DoesNotContain( + "LocalPlayerControllerSlot", + app, + StringComparison.Ordinal); + Assert.DoesNotContain( + "ILocalPlayerControllerSource", + app, + StringComparison.Ordinal); + Assert.False(File.Exists(Path.Combine( + appRoot, + "Physics", + "RemotePhysicsBodyInitializer.cs"))); + Assert.False(typeof(AcDream.App.World.LiveEntityRecord) + .GetProperty(nameof(AcDream.App.World.LiveEntityRecord.PhysicsBody))! + .CanWrite); + Assert.False(typeof(AcDream.App.World.LiveEntityRecord) + .GetProperty(nameof(AcDream.App.World.LiveEntityRecord.RemoteMotionRuntime))! + .CanWrite); + Assert.False(typeof(AcDream.App.World.LiveEntityRecord) + .GetProperty(nameof(AcDream.App.World.LiveEntityRecord.PhysicsHost))! + .CanWrite); } private static string FindRepositoryRoot() diff --git a/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs b/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs index 2ef2dffb..8689d125 100644 --- a/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs +++ b/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs @@ -411,7 +411,7 @@ public sealed class LocalPlayerTeleportControllerTests MeshRefs = Array.Empty(), })!; var identity = new LocalPlayerIdentityState { ServerGuid = guid }; - var controllerSlot = new LocalPlayerControllerSlot + var controllerSlot = new RuntimeLocalPlayerMovementState { Controller = new PlayerMovementController(new PhysicsEngine()), }; @@ -494,7 +494,7 @@ public sealed class LocalPlayerTeleportControllerTests MeshRefs = Array.Empty(), })!; var identity = new LocalPlayerIdentityState { ServerGuid = guid }; - var controllerSlot = new LocalPlayerControllerSlot + var controllerSlot = new RuntimeLocalPlayerMovementState { Controller = new PlayerMovementController(new PhysicsEngine()), }; diff --git a/tests/AcDream.App.Tests/Streaming/StreamingFrameControllerTests.cs b/tests/AcDream.App.Tests/Streaming/StreamingFrameControllerTests.cs index 2e608a5f..3b4e9cfd 100644 --- a/tests/AcDream.App.Tests/Streaming/StreamingFrameControllerTests.cs +++ b/tests/AcDream.App.Tests/Streaming/StreamingFrameControllerTests.cs @@ -422,7 +422,7 @@ public sealed class StreamingFrameControllerTests } internal LocalPlayerModeState Mode { get; } = new(); - internal LocalPlayerControllerSlot Player { get; } = new(); + internal RuntimeLocalPlayerMovementState Player { get; } = new(); internal SessionSource Session { get; } = new(); internal LiveWorldOriginState Origin { get; } = new(); internal PlayerLandblockSource PlayerLandblock { get; } = new(); diff --git a/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs b/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs index 99f5ecc5..6827ed6e 100644 --- a/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs +++ b/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs @@ -144,12 +144,7 @@ public sealed class GameWindowLiveEntityCompositionTests typeof(LiveEntityAnimationRuntimeView) .GetFields(BindingFlags.Instance | BindingFlags.NonPublic), field => field.Name == "_runtime"); - FieldInfo remoteRuntime = Assert.Single( - typeof(LiveEntityRemoteMotionRuntimeView) - .GetFields(BindingFlags.Instance | BindingFlags.NonPublic), - field => field.Name == "_runtime"); Assert.Equal(typeof(ILiveEntityRuntimeSource), animationRuntime.FieldType); - Assert.Equal(typeof(ILiveEntityRuntimeSource), remoteRuntime.FieldType); FieldInfo[] projectileFields = typeof(ProjectileController).GetFields( BindingFlags.Instance | BindingFlags.NonPublic); @@ -192,7 +187,7 @@ public sealed class GameWindowLiveEntityCompositionTests source, StringComparison.Ordinal); Assert.DoesNotContain( - "new LiveEntityRemoteMotionRuntimeView(() =>", + "LiveEntityRemoteMotionRuntimeView", source, StringComparison.Ordinal); Assert.Contains("new DatProjectileSetupResolver", livePresentation); diff --git a/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs b/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs index b74f8ac6..b108ae84 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs @@ -371,7 +371,11 @@ public sealed class LiveEntityHydrationControllerTests LiveEntityRecord record = fixture.Record; WorldEntity entity = record.WorldEntity!; var body = new PhysicsBody(); - record.PhysicsBody = body; + Assert.Same( + body, + fixture.Runtime.GetOrCreatePhysicsBody( + record.ServerGuid, + _ => body)); fixture.Relationships.OnUnparentAction = _ => fixture.Runtime.WithdrawLiveEntityProjection(record) ? ChildUnparentDisposition.Completed diff --git a/tests/AcDream.App.Tests/World/LiveEntityLifecycleStressTests.cs b/tests/AcDream.App.Tests/World/LiveEntityLifecycleStressTests.cs index 14fc2a48..3bdfdd62 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityLifecycleStressTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityLifecycleStressTests.cs @@ -874,7 +874,7 @@ public sealed class LiveEntityLifecycleStressTests _animations.TryGetValue(id, out Animation? animation) ? animation : null; } - private sealed class TestRemoteMotion : ILiveEntityRemotePlacementRuntime + private sealed class TestRemoteMotion : IRuntimeRemotePlacement { private uint _cellId; private Func? _readCell; diff --git a/tests/AcDream.App.Tests/World/LiveEntityPhysicsHostOwnershipTests.cs b/tests/AcDream.App.Tests/World/LiveEntityPhysicsHostOwnershipTests.cs index f367a2c7..53a849d1 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityPhysicsHostOwnershipTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityPhysicsHostOwnershipTests.cs @@ -710,8 +710,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests interruptCurrentMovement); private sealed class HostConsumerMotion : - ILiveEntityRemoteMotionRuntime, - ILiveEntityPhysicsHostConsumer + IRuntimeRemoteMotion, + IRuntimePhysicsHostConsumer { private Func? _read; public PhysicsBody Body { get; } = new(); @@ -725,8 +725,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests } private sealed class ThrowingCellConsumerMotion : - ILiveEntityRemoteMotionRuntime, - ILiveEntityCanonicalRuntimeConsumer + IRuntimeRemoteMotion, + IRuntimeCanonicalPhysicsConsumer { private bool _failOnce = true; public PhysicsBody Body { get; } = new(); @@ -748,14 +748,14 @@ public sealed class LiveEntityPhysicsHostOwnershipTests private sealed class AlternatingBodyMotion( PhysicsBody first, - PhysicsBody second) : ILiveEntityRemoteMotionRuntime + PhysicsBody second) : IRuntimeRemoteMotion { private int _reads; public PhysicsBody Body => _reads++ == 0 ? first : second; } private sealed class SequenceBodyMotion(params PhysicsBody[] bodies) : - ILiveEntityRemoteMotionRuntime + IRuntimeRemoteMotion { public int ReadCount { get; private set; } @@ -771,8 +771,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests } private sealed class CallbackCanonicalMotion(Action callback) : - ILiveEntityRemoteMotionRuntime, - ILiveEntityCanonicalRuntimeConsumer + IRuntimeRemoteMotion, + IRuntimeCanonicalPhysicsConsumer { public PhysicsBody Body { get; } = new(); diff --git a/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs b/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs index cd689b67..1f43e606 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs @@ -48,7 +48,7 @@ public sealed class LiveEntityRuntimeTests private sealed class EffectProfile : ILiveEntityEffectProfile { } - private sealed class RemoteMotionRuntime : ILiveEntityRemoteMotionRuntime + private sealed class RemoteMotionRuntime : IRuntimeRemoteMotion { public PhysicsBody Body { get; } = new(); } @@ -1156,43 +1156,6 @@ public sealed class LiveEntityRuntimeTests Assert.False(remote.Body.TransientState.HasFlag(TransientStateFlags.Active)); } - [Fact] - public void MovementActivation_ReactivatesExactIncarnation_ButStaticIsNoOp() - { - const uint ordinaryGuid = 0x70000050u; - const uint staticGuid = 0x70000051u; - var spatial = new GpuWorldState(); - spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); - - LiveEntityRecord ordinary = RegisterProjection( - runtime, - Spawn(ordinaryGuid, 1, 1, 0x01010001u)); - var ordinaryRemote = new RemoteMotionRuntime(); - runtime.SetRemoteMotionRuntime(ordinaryGuid, ordinaryRemote); - ordinary.ObjectClock.Deactivate(); - ordinaryRemote.Body.TransientState &= ~TransientStateFlags.Active; - - Assert.True(runtime.TryActivateOrdinaryObject(ordinaryGuid, ordinaryRemote)); - Assert.True(ordinary.ObjectClock.IsActive); - Assert.True(ordinaryRemote.Body.TransientState.HasFlag(TransientStateFlags.Active)); - - LiveEntityRecord staticRecord = RegisterProjection( - runtime, - Spawn( - staticGuid, - 1, - 1, - 0x01010001u, - PhysicsStateFlags.Static)); - var staticRemote = new RemoteMotionRuntime(); - runtime.SetRemoteMotionRuntime(staticGuid, staticRemote); - - Assert.False(runtime.TryActivateOrdinaryObject(staticGuid, staticRemote)); - Assert.False(staticRecord.ObjectClock.IsActive); - Assert.False(staticRemote.Body.TransientState.HasFlag(TransientStateFlags.Active)); - } - [Fact] public void AuthoritativeVector_WakesOrdinaryClockButPreservesStaticActivity() { diff --git a/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs b/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs index 931b3640..1c9217d7 100644 --- a/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs +++ b/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs @@ -244,6 +244,10 @@ public sealed class RuntimeEntityOwnershipTests Assert.Contains( viewFields, field => field.FieldType == typeof(IRuntimeInventoryView)); + Assert.DoesNotContain( + viewFields, + field => field.FieldType == typeof(LiveEntityRuntime) + || field.FieldType == typeof(ClientObjectTable)); Assert.DoesNotContain( typeof(CurrentGameRuntimeViewAdapter).GetNestedTypes( BindingFlags.NonPublic), @@ -271,6 +275,21 @@ public sealed class RuntimeEntityOwnershipTests Assert.DoesNotContain("_entityObjects.PublishEntity", liveSource); Assert.DoesNotContain("_directory.TryApply", liveSource); Assert.DoesNotContain("_directory.RemoveActive", liveSource); + + string viewSource = File.ReadAllText(Path.Combine( + root, + "src", + "AcDream.App", + "Runtime", + "CurrentGameRuntimeViewAdapter.cs")); + Assert.Contains( + "_entityView.MaterializedCount", + viewSource, + StringComparison.Ordinal); + Assert.DoesNotContain( + "_entities.MaterializedCount", + viewSource, + StringComparison.Ordinal); } private static bool IsPresentationType(Type type) diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityObjectLifetimeTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityObjectLifetimeTests.cs index bc6a3968..f4732dd2 100644 --- a/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityObjectLifetimeTests.cs +++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityObjectLifetimeTests.cs @@ -1123,7 +1123,10 @@ public sealed class RuntimeEntityObjectLifetimeTests lifetime.Dispose(); Assert.Equal( - default(RuntimeEntityObjectOwnershipSnapshot), + default(RuntimeEntityObjectOwnershipSnapshot) with + { + IsDisposed = true, + }, lifetime.CaptureOwnership()); } @@ -1158,7 +1161,10 @@ public sealed class RuntimeEntityObjectLifetimeTests lifetime.Dispose(); Assert.Equal( - default(RuntimeEntityObjectOwnershipSnapshot), + default(RuntimeEntityObjectOwnershipSnapshot) with + { + IsDisposed = true, + }, lifetime.CaptureOwnership()); } diff --git a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeGameplayOwnershipTests.cs b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeGameplayOwnershipTests.cs index ecbca098..4f39070b 100644 --- a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeGameplayOwnershipTests.cs +++ b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeGameplayOwnershipTests.cs @@ -3,11 +3,60 @@ using AcDream.Core.Items; using AcDream.Core.Social; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; +using AcDream.Runtime; namespace AcDream.Runtime.Tests.Gameplay; public sealed class RuntimeGameplayOwnershipTests { + [Fact] + public void J5LedgerConvergesEntityPhysicsAndGameplayAsOneLifetime() + { + var entities = new RuntimeEntityObjectLifetime(); + var inventory = new RuntimeInventoryState(entities); + var character = new RuntimeCharacterState(); + var communication = new RuntimeCommunicationState(); + var actions = RuntimeActionTestFactory.Create(inventory.Transactions); + var movement = new RuntimeLocalPlayerMovementState(); + + movement.Execute(RuntimeMovementCommand.ToggleRunLock); + inventory.Transactions.IncrementBusyCount(); + actions.Selection.Select( + 0x70000001u, + AcDream.Core.Selection.SelectionChangeSource.System); + character.Spellbook.OnSpellLearned(42u); + communication.Chat.OnSystemMessage("runtime-only", 0u); + + RuntimeSimulationOwnershipSnapshot populated = + RuntimeSimulationOwnership.Capture( + entities, + inventory, + character, + communication, + actions, + movement); + Assert.False(populated.IsConverged); + + movement.Dispose(); + actions.Dispose(); + communication.Dispose(); + character.Dispose(); + inventory.Dispose(); + entities.Dispose(); + + RuntimeSimulationOwnershipSnapshot retired = + RuntimeSimulationOwnership.Capture( + entities, + inventory, + character, + communication, + actions, + movement); + Assert.True(retired.IsConverged); + Assert.True(retired.EntityObjects.IsDisposed); + Assert.True(retired.Physics.IsDisposed); + } + [Fact] public void CombinedLedgerConvergesEveryGameplayOwnerAndSubscription() { diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs index 6a245381..fe577c97 100644 --- a/tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs @@ -340,6 +340,49 @@ public sealed class RuntimePhysicsStateTests Assert.Equal(0, first.Physics.Engine.LandblockCount); } + [Fact] + public void TerminalDisposalClearsLandblocksShadowsAndWorksets() + { + var lifetime = new RuntimeEntityObjectLifetime(); + RuntimeEntityRecord record = + lifetime.Entities.AddActive(Spawn(0x70000041u, 1)); + lifetime.Physics.AcknowledgeSpatialProjection( + record, + spatial: true); + + RuntimeCollisionAdmission admission = + lifetime.Physics.BeginCollisionAdmission(0x0101FFFFu); + lifetime.Physics.AdmitCollisionAssets( + admission, + CollisionAssets(0x0101FFFFu)); + _ = lifetime.Physics.CompleteCollisionAdmission(admission); + lifetime.Physics.Engine.ShadowObjects.Register( + entityId: record.LocalEntityId!.Value, + gfxObjId: 0x01000001u, + worldPos: new Vector3(10f, 20f, 5f), + rotation: Quaternion.Identity, + radius: 0.5f, + worldOffsetX: 0f, + worldOffsetY: 0f, + landblockId: 0x0101FFFFu, + seedCellId: 0x01010001u, + isStatic: false); + + Assert.False(lifetime.Physics.CaptureOwnership().IsConverged); + Assert.True( + lifetime.Physics.Engine.ShadowObjects + .RetainedRegistrationCount > 0); + + lifetime.Dispose(); + + RuntimePhysicsOwnershipSnapshot retired = + lifetime.Physics.CaptureOwnership(); + Assert.True(retired.IsConverged); + Assert.Equal(0, retired.LandblockCount); + Assert.Equal(0, retired.RetainedShadowRegistrationCount); + Assert.Equal(0, retired.SpatialRootCount); + } + [Fact] public void RemoteBindingOwnsCanonicalBodyCellAndTypedCellPublication() { @@ -368,6 +411,66 @@ public sealed class RuntimePhysicsStateTests Assert.Equal(0, lifetime.Physics.SpatialRemoteCount); } + [Fact] + public void BuiltInRemoteConstructionInstallsCreateVectorsOnlyOnce() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + RuntimeEntityRecord record = lifetime.Entities.AddActive( + Spawn( + 0x70000042u, + 1, + velocity: new Vector3(40f, 0f, 0f), + angularVelocity: new Vector3(0f, 0f, 2f))); + + RemoteMotion remote = + lifetime.Physics.GetOrCreateRemoteMotion(record); + + Assert.Equal(new Vector3(40f, 0f, 0f), remote.Body.Velocity); + Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega); + remote.Body.set_velocity(new Vector3(8f, 0f, 0f)); + remote.Body.Omega = new Vector3(0f, 0f, 3f); + + Assert.Same( + remote, + lifetime.Physics.GetOrCreateRemoteMotion(record)); + Assert.Equal(new Vector3(8f, 0f, 0f), remote.Body.Velocity); + Assert.Equal(new Vector3(0f, 0f, 3f), remote.Body.Omega); + } + + [Fact] + public void BuiltInMovementActivationUsesExactRuntimeIncarnation() + { + using var lifetime = new RuntimeEntityObjectLifetime(); + RuntimeEntityRecord ordinary = + lifetime.Entities.AddActive(Spawn(0x70000043u, 1)); + RemoteMotion ordinaryRemote = + lifetime.Physics.GetOrCreateRemoteMotion(ordinary); + ordinary.ObjectClock.Deactivate(); + ordinaryRemote.Body.TransientState &= + ~TransientStateFlags.Active; + + ordinaryRemote.Movement.ActivatePhysicsObject!(); + + Assert.True(ordinary.ObjectClock.IsActive); + Assert.True(ordinaryRemote.Body.IsActive); + + RuntimeEntityRecord staticRecord = + lifetime.Entities.AddActive(Spawn(0x70000044u, 1)); + lifetime.Entities.SetFinalPhysicsState( + staticRecord, + PhysicsStateFlags.Static); + RemoteMotion staticRemote = + lifetime.Physics.GetOrCreateRemoteMotion(staticRecord); + staticRecord.ObjectClock.Deactivate(); + staticRemote.Body.TransientState &= + ~TransientStateFlags.Active; + + staticRemote.Movement.ActivatePhysicsObject!(); + + Assert.False(staticRecord.ObjectClock.IsActive); + Assert.False(staticRemote.Body.IsActive); + } + [Fact] public void RemotePhysicsTickUsesCanonicalRuntimeAndPublishesPoseSnapshot() { @@ -523,7 +626,11 @@ public sealed class RuntimePhysicsStateTests 0u); } - private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance) + private static WorldSession.EntitySpawn Spawn( + uint guid, + ushort instance, + Vector3? velocity = null, + Vector3? angularVelocity = null) { var position = new CreateObject.ServerPosition( 0x0101FFFFu, @@ -559,9 +666,9 @@ public sealed class RuntimePhysicsStateTests Friction: null, Elasticity: null, Translucency: null, - Velocity: null, + Velocity: velocity, Acceleration: null, - AngularVelocity: null, + AngularVelocity: angularVelocity, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); diff --git a/tests/AcDream.Runtime.Tests/RuntimeSimulationFixtureHostTests.cs b/tests/AcDream.Runtime.Tests/RuntimeSimulationFixtureHostTests.cs new file mode 100644 index 00000000..145d32c4 --- /dev/null +++ b/tests/AcDream.Runtime.Tests/RuntimeSimulationFixtureHostTests.cs @@ -0,0 +1,384 @@ +using System.Numerics; +using AcDream.Core.Combat; +using AcDream.Core.Items; +using AcDream.Core.Net; +using AcDream.Core.Net.Messages; +using AcDream.Core.Physics; +using AcDream.Core.Spells; +using AcDream.Runtime.Entities; +using AcDream.Runtime.Gameplay; +using AcDream.Runtime.Physics; + +namespace AcDream.Runtime.Tests; + +public sealed class RuntimeSimulationFixtureHostTests +{ + [Fact] + public void RuntimeOnlyHostDrivesCompleteJ5SimulationWithoutPresentation() + { + var host = new RuntimeOnlySimulationFixtureHost(); + + host.Move(); + host.Use(0x70000011u); + host.Attack(); + host.Cast(); + host.AdvanceProjectile(failProjection: false); + + Assert.Equal( + [ + "movement:run", + "use:70000011", + "attack:prepare", + "attack:Medium:0.5", + "cast:stop", + "cast:1", + "cast:busy", + "projectile:ack", + ], + host.Trace); + + string[] loaded = AppDomain.CurrentDomain.GetAssemblies() + .Select(static assembly => + assembly.GetName().Name ?? string.Empty) + .ToArray(); + Assert.DoesNotContain(loaded, static name => + name.StartsWith( + "AcDream.App", + StringComparison.OrdinalIgnoreCase) + || name.StartsWith( + "AcDream.UI.", + StringComparison.OrdinalIgnoreCase) + || name.StartsWith( + "Silk.NET", + StringComparison.OrdinalIgnoreCase) + || name.StartsWith( + "OpenAL", + StringComparison.OrdinalIgnoreCase) + || name.StartsWith( + "ImGui", + StringComparison.OrdinalIgnoreCase)); + + host.Dispose(); + Assert.True(host.CaptureOwnership().IsConverged); + } + + [Fact] + public void ProjectionAcknowledgementFailureStillAllowsTerminalConvergence() + { + var host = new RuntimeOnlySimulationFixtureHost(); + + Assert.Throws(() => + host.AdvanceProjectile(failProjection: true)); + + host.Dispose(); + + RuntimeSimulationOwnershipSnapshot ownership = + host.CaptureOwnership(); + Assert.True(ownership.IsConverged); + Assert.Equal(0, ownership.Physics.SpatialProjectileCount); + Assert.Equal(0, ownership.EntityObjects.ActiveEntityCount); + } + + private sealed class RuntimeOnlySimulationFixtureHost : + IRuntimeInteractionTransport, + IRuntimeCombatAttackOperations, + IRuntimeCombatTargetOperations, + IRuntimeCombatModeOperations, + IRuntimeSpellCastOperations, + IDisposable + { + private const uint ProjectileGuid = 0x70000021u; + private double _now = 10d; + private uint _sequence; + private bool _disposed; + + public RuntimeOnlySimulationFixtureHost() + { + EntityObjects = new RuntimeEntityObjectLifetime(); + Inventory = new RuntimeInventoryState(EntityObjects); + Character = new RuntimeCharacterState(); + Character.Spellbook.InstallMetadata(SpellMetadata()); + Character.Spellbook.OnSpellLearned(1u); + Communication = new RuntimeCommunicationState(); + Actions = new RuntimeActionState( + Inventory.Transactions, + Character.Spellbook, + this, + this, + this, + this, + now: () => _now); + Movement = new RuntimeLocalPlayerMovementState(); + } + + public RuntimeEntityObjectLifetime EntityObjects { get; } + public RuntimeInventoryState Inventory { get; } + public RuntimeCharacterState Character { get; } + public RuntimeCommunicationState Communication { get; } + public RuntimeActionState Actions { get; } + public RuntimeLocalPlayerMovementState Movement { get; } + public List Trace { get; } = []; + + public void Move() + { + Assert.True(Movement.Execute( + RuntimeMovementCommand.ToggleRunLock)); + Assert.True(Movement.AutoRunActive); + Trace.Add("movement:run"); + } + + public void Use(uint serverGuid) + { + ItemUseRequestReservation reservation = + Actions.Transactions.BeginUseRequestReservation(); + RuntimeInteractionDispatchResult result = + Actions.Transactions.TryDispatchUse( + serverGuid, + ownedByPlayer: false, + useable: true, + reservation, + this, + out _); + Assert.Equal( + RuntimeInteractionDispatchResult.Dispatched, + result); + Actions.Transactions.CompleteUse(0u); + } + + public void Attack() + { + Actions.Combat.SetCombatMode(CombatMode.Melee); + Actions.CombatAttack.SetDesiredPower(0.5f); + Actions.CombatAttack.PressAttack(AttackHeight.Medium); + _now += 0.5d; + Actions.CombatAttack.ReleaseAttack(); + } + + public void Cast() => + Assert.Equal( + CastRequestResult.Sent, + Actions.SpellCast.Cast(1u)); + + public void AdvanceProjectile(bool failProjection) + { + RuntimeEntityRecord record = + EntityObjects.RegisterEntity( + Spawn(ProjectileGuid, 1)).Canonical!; + PhysicsBody body = + EntityObjects.Physics.GetOrCreatePhysicsBody( + record, + static canonical => + { + var created = new PhysicsBody + { + Position = new Vector3(10f, 20f, 5f), + Orientation = Quaternion.Identity, + InWorld = true, + TransientState = + TransientStateFlags.Active, + }; + created.SnapToCell( + canonical.FullCellId, + created.Position, + created.Position); + return created; + }); + _ = EntityObjects.Physics.BindProjectile( + record, + body, + new ProjectileCollisionSphere( + Vector3.Zero, + 0.25f)); + EntityObjects.Physics.AcknowledgeSpatialProjection( + record, + spatial: true); + + var updater = + new RuntimeProjectilePhysicsUpdater( + EntityObjects.Physics); + Assert.True(updater.TryBegin( + record, + quantum: 0.05f, + record.ObjectClockEpoch, + externalOwnerValid: null, + out RuntimeProjectilePhysicsCommit commit)); + _ = updater.Complete( + commit, + liveCenterX: 1, + liveCenterY: 1, + _ => + { + if (failProjection) + { + throw new InvalidOperationException( + "projection acknowledgement"); + } + Trace.Add("projectile:ack"); + return true; + }); + } + + public RuntimeSimulationOwnershipSnapshot CaptureOwnership() => + RuntimeSimulationOwnership.Capture( + EntityObjects, + Inventory, + Character, + Communication, + Actions, + Movement); + + public bool IsInWorld => true; + public uint LocalPlayerId => 0x50000001u; + public bool CanSend => true; + public bool IsDualWield => false; + public bool PlayerReadyForAttack => true; + public bool AutoRepeatAttack => false; + public bool AutoTarget => false; + + public bool TrySendUse(uint serverGuid, out uint sequence) + { + sequence = ++_sequence; + Trace.Add($"use:{serverGuid:X8}"); + return true; + } + + public bool TrySendPickup( + uint itemGuid, + uint destinationContainerId, + int placement, + out uint sequence) + { + sequence = ++_sequence; + return true; + } + + public bool CanStartAttack() => true; + public void PrepareAttackRequest() => + Trace.Add("attack:prepare"); + + public bool SendAttack(AttackHeight height, float power) + { + Trace.Add($"attack:{height}:{power:0.0}"); + return true; + } + + public void SendCancelAttack() { } + public uint? SelectClosestTarget() => null; + public IReadOnlyList GetOrderedEquipment() => []; + public void NotifyExplicitCombatModeRequest() { } + public void SendChangeCombatMode(CombatMode mode) { } + public bool HasRequiredComponents(uint spellId) => true; + + public bool IsTargetCompatible( + uint targetId, + SpellMetadata spell, + bool showMessage) => true; + + public void StopCompletely() => + Trace.Add("cast:stop"); + + public void SendUntargeted(uint spellId) => + Trace.Add($"cast:{spellId}"); + + public void SendTargeted(uint targetId, uint spellId) => + Trace.Add($"cast:{targetId}:{spellId}"); + + public void DisplayMessage(string message) => + Trace.Add($"message:{message}"); + + public void IncrementBusy() + { + Inventory.Transactions.IncrementBusyCount(); + Trace.Add("cast:busy"); + } + + public void Dispose() + { + if (_disposed) + return; + _disposed = true; + Movement.Dispose(); + Actions.Dispose(); + Communication.Dispose(); + Character.Dispose(); + Inventory.Dispose(); + EntityObjects.Dispose(); + } + + private static SpellTable SpellMetadata() + { + const string header = + "Spell ID,Name,Flags [Hex],IsUntargetted,TargetMask [Hex]"; + const string row = "1,Runtime Fixture,0x0,true,0x0"; + return SpellTable.LoadFromReader( + new System.IO.StringReader($"{header}\n{row}")); + } + + private static WorldSession.EntitySpawn Spawn( + uint guid, + ushort instance) + { + var position = new CreateObject.ServerPosition( + 0x01010001u, + 10f, + 20f, + 5f, + 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: instance); + var physics = new PhysicsSpawnData( + RawState: (uint)( + PhysicsStateFlags.ReportCollisions + | PhysicsStateFlags.Missile), + Position: position, + Movement: null, + AnimationFrame: null, + SetupTableId: 0x02000001u, + MotionTableId: null, + SoundTableId: null, + PhysicsScriptTableId: null, + Parent: null, + Children: null, + Scale: null, + Friction: null, + Elasticity: null, + Translucency: null, + Velocity: new Vector3(10f, 0f, 0f), + Acceleration: null, + AngularVelocity: null, + DefaultScriptType: null, + DefaultScriptIntensity: null, + Timestamps: timestamps); + return new WorldSession.EntitySpawn( + guid, + position, + 0x02000001u, + Array.Empty(), + Array.Empty(), + Array.Empty(), + null, + null, + "runtime projectile", + null, + null, + null, + PhysicsState: physics.RawState, + InstanceSequence: instance, + MovementSequence: 1, + ServerControlSequence: 1, + PositionSequence: 1, + Physics: physics); + } + } +}