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.
This commit is contained in:
Erik 2026-07-26 15:53:31 +02:00
parent c30a3efeb0
commit cdee7a4b49
57 changed files with 1013 additions and 410 deletions

View file

@ -122,7 +122,7 @@ internal sealed class LiveCombatAttackOperations
private readonly CombatState _combat; private readonly CombatState _combat;
private readonly ICombatAttackTargetSource _targets; private readonly ICombatAttackTargetSource _targets;
private readonly ICombatGameplaySettingsSource _settings; private readonly ICombatGameplaySettingsSource _settings;
private readonly ILocalPlayerControllerSource _player; private readonly IRuntimeLocalPlayerControllerSource _player;
private readonly LocalPlayerOutboundController _outbound; private readonly LocalPlayerOutboundController _outbound;
private readonly ILiveInWorldSource _inWorld; private readonly ILiveInWorldSource _inWorld;
private readonly ILiveWorldSessionSource _session; private readonly ILiveWorldSessionSource _session;
@ -132,7 +132,7 @@ internal sealed class LiveCombatAttackOperations
CombatState combat, CombatState combat,
ICombatAttackTargetSource targets, ICombatAttackTargetSource targets,
ICombatGameplaySettingsSource settings, ICombatGameplaySettingsSource settings,
ILocalPlayerControllerSource player, IRuntimeLocalPlayerControllerSource player,
LocalPlayerOutboundController outbound, LocalPlayerOutboundController outbound,
ILiveInWorldSource inWorld, ILiveInWorldSource inWorld,
ILiveWorldSessionSource session, ILiveWorldSessionSource session,

View file

@ -33,7 +33,7 @@ internal sealed record FrameRootDependencies(
LocalPlayerModeState PlayerMode, LocalPlayerModeState PlayerMode,
LocalPlayerIdentityState PlayerIdentity, LocalPlayerIdentityState PlayerIdentity,
ChaseCameraInputState ChaseCameraInput, ChaseCameraInputState ChaseCameraInput,
LocalPlayerControllerSlot PlayerController, RuntimeLocalPlayerMovementState PlayerController,
LiveWorldOriginState WorldOrigin, LiveWorldOriginState WorldOrigin,
ParticleVisibilityController ParticleVisibility, ParticleVisibilityController ParticleVisibility,
EntityEffectPoseRegistry EffectPoses, EntityEffectPoseRegistry EffectPoses,

View file

@ -56,7 +56,7 @@ internal sealed record InteractionRetainedUiDependencies(
BufferedUiRegistry? UiRegistry, BufferedUiRegistry? UiRegistry,
LiveCombatModeCommandSlot CombatModeCommands, LiveCombatModeCommandSlot CombatModeCommands,
ILocalPlayerIdentitySource PlayerIdentity, ILocalPlayerIdentitySource PlayerIdentity,
ILocalPlayerControllerSource PlayerController, IRuntimeLocalPlayerControllerSource PlayerController,
ILocalPlayerModeSource PlayerMode, ILocalPlayerModeSource PlayerMode,
Func<DeferredSelectionViewPlaneSource, SelectionCameraSource> Func<DeferredSelectionViewPlaneSource, SelectionCameraSource>
SelectionCameraFactory, SelectionCameraFactory,

View file

@ -60,7 +60,7 @@ internal sealed record LivePresentationDependencies(
CellVisibility CellVisibility, CellVisibility CellVisibility,
LiveWorldOriginState WorldOrigin, LiveWorldOriginState WorldOrigin,
LocalPlayerIdentityState PlayerIdentity, LocalPlayerIdentityState PlayerIdentity,
LocalPlayerControllerSlot PlayerController, RuntimeLocalPlayerMovementState PlayerController,
PointerPositionState PointerPosition, PointerPositionState PointerPosition,
PlayerApproachCompletionState PlayerApproachCompletions, PlayerApproachCompletionState PlayerApproachCompletions,
GameRenderResourceLifetime RenderResourceLifetime, GameRenderResourceLifetime RenderResourceLifetime,

View file

@ -60,7 +60,7 @@ internal sealed record SessionPlayerDependencies(
RetailInboundEventDispatcher InboundEntityEvents, RetailInboundEventDispatcher InboundEntityEvents,
DeferredLiveEntityMotionRuntimeBindings MotionBindings, DeferredLiveEntityMotionRuntimeBindings MotionBindings,
LocalPlayerIdentityState PlayerIdentity, LocalPlayerIdentityState PlayerIdentity,
LocalPlayerControllerSlot PlayerController, RuntimeLocalPlayerMovementState PlayerController,
LocalPlayerPhysicsHostSlot PlayerHost, LocalPlayerPhysicsHostSlot PlayerHost,
LocalPlayerModeState PlayerMode, LocalPlayerModeState PlayerMode,
ChaseCameraInputState ChaseCameraInput, ChaseCameraInputState ChaseCameraInput,
@ -507,7 +507,6 @@ internal sealed class SessionPlayerCompositionPhase
live.ProjectileController, live.ProjectileController,
live.RemoteTeleport, live.RemoteTeleport,
d.AnimatedEntities, d.AnimatedEntities,
new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(d.RuntimeSlot),
d.RemoteMovementObservations, d.RemoteMovementObservations,
d.RemotePhysicsUpdater, d.RemotePhysicsUpdater,
d.RemoteInboundMotion, d.RemoteInboundMotion,
@ -874,7 +873,6 @@ internal sealed class SessionPlayerCompositionPhase
sessionHost, sessionHost,
liveSessionCommands, liveSessionCommands,
d.PlayerIdentity, d.PlayerIdentity,
live.LiveEntities,
d.EntityObjects, d.EntityObjects,
d.Inventory, d.Inventory,
d.Character, d.Character,

View file

@ -162,7 +162,7 @@ internal interface INearbyWorldDiagnosticSource
internal sealed class RuntimeNearbyWorldDiagnosticSource : INearbyWorldDiagnosticSource internal sealed class RuntimeNearbyWorldDiagnosticSource : INearbyWorldDiagnosticSource
{ {
private readonly ILocalPlayerModeSource _mode; private readonly ILocalPlayerModeSource _mode;
private readonly ILocalPlayerControllerSource _player; private readonly IRuntimeLocalPlayerControllerSource _player;
private readonly CameraController _camera; private readonly CameraController _camera;
private readonly LiveWorldOriginState _origin; private readonly LiveWorldOriginState _origin;
private readonly GpuWorldState _world; private readonly GpuWorldState _world;
@ -170,7 +170,7 @@ internal sealed class RuntimeNearbyWorldDiagnosticSource : INearbyWorldDiagnosti
public RuntimeNearbyWorldDiagnosticSource( public RuntimeNearbyWorldDiagnosticSource(
ILocalPlayerModeSource mode, ILocalPlayerModeSource mode,
ILocalPlayerControllerSource player, IRuntimeLocalPlayerControllerSource player,
CameraController camera, CameraController camera,
LiveWorldOriginState origin, LiveWorldOriginState origin,
GpuWorldState world, GpuWorldState world,

View file

@ -1,18 +1,4 @@
global using AcDream.Runtime.Gameplay; global using AcDream.Runtime.Gameplay;
global using AcDream.Runtime.Physics; 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 = global using ILocalPlayerMotionSource =
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource; AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource;

View file

@ -35,7 +35,7 @@ internal sealed class LiveLocalPlayerFrameRuntime : ILocalPlayerFrameRuntime
{ {
private readonly CameraController _camera; private readonly CameraController _camera;
private readonly ILocalPlayerModeSource _mode; private readonly ILocalPlayerModeSource _mode;
private readonly ILocalPlayerControllerSource _controller; private readonly IRuntimeLocalPlayerControllerSource _controller;
private readonly IChaseCameraSource _chase; private readonly IChaseCameraSource _chase;
private readonly DispatcherMovementInputSource _input; private readonly DispatcherMovementInputSource _input;
private readonly IInputCaptureSource _capture; private readonly IInputCaptureSource _capture;
@ -49,7 +49,7 @@ internal sealed class LiveLocalPlayerFrameRuntime : ILocalPlayerFrameRuntime
public LiveLocalPlayerFrameRuntime( public LiveLocalPlayerFrameRuntime(
CameraController camera, CameraController camera,
ILocalPlayerModeSource mode, ILocalPlayerModeSource mode,
ILocalPlayerControllerSource controller, IRuntimeLocalPlayerControllerSource controller,
IChaseCameraSource chase, IChaseCameraSource chase,
DispatcherMovementInputSource input, DispatcherMovementInputSource input,
IInputCaptureSource capture, IInputCaptureSource capture,

View file

@ -96,7 +96,7 @@ internal sealed class MouseLookController : IMouseLookInputFrameController
private readonly IMouseSource _mouseSource; private readonly IMouseSource _mouseSource;
private readonly IPointerPositionSource _pointer; private readonly IPointerPositionSource _pointer;
private readonly ILocalPlayerModeSource _playerMode; private readonly ILocalPlayerModeSource _playerMode;
private readonly ILocalPlayerControllerSource _playerController; private readonly IRuntimeLocalPlayerControllerSource _playerController;
private readonly CameraController _camera; private readonly CameraController _camera;
private readonly ChaseCameraInputState _chase; private readonly ChaseCameraInputState _chase;
private readonly IMovementInputSource _movementInput; private readonly IMovementInputSource _movementInput;
@ -111,7 +111,7 @@ internal sealed class MouseLookController : IMouseLookInputFrameController
IMouseSource mouseSource, IMouseSource mouseSource,
IPointerPositionSource pointer, IPointerPositionSource pointer,
ILocalPlayerModeSource playerMode, ILocalPlayerModeSource playerMode,
ILocalPlayerControllerSource playerController, IRuntimeLocalPlayerControllerSource playerController,
CameraController camera, CameraController camera,
ChaseCameraInputState chase, ChaseCameraInputState chase,
IMovementInputSource movementInput, IMovementInputSource movementInput,

View file

@ -7,7 +7,7 @@ internal sealed class MovementTruthDiagnosticController
: IMovementTruthDiagnosticSink : IMovementTruthDiagnosticSink
{ {
private readonly bool _enabled; private readonly bool _enabled;
private readonly ILocalPlayerControllerSource _player; private readonly IRuntimeLocalPlayerControllerSource _player;
private readonly ILocalPlayerIdentitySource _identity; private readonly ILocalPlayerIdentitySource _identity;
private MovementTruthOutbound? _lastOutbound; private MovementTruthOutbound? _lastOutbound;
@ -24,7 +24,7 @@ internal sealed class MovementTruthDiagnosticController
public MovementTruthDiagnosticController( public MovementTruthDiagnosticController(
bool enabled, bool enabled,
ILocalPlayerControllerSource player, IRuntimeLocalPlayerControllerSource player,
ILocalPlayerIdentitySource identity) ILocalPlayerIdentitySource identity)
{ {
_enabled = enabled; _enabled = enabled;

View file

@ -23,7 +23,7 @@ internal sealed class PlayerModeController :
IDevToolsPlayerModeTarget IDevToolsPlayerModeTarget
{ {
private readonly LocalPlayerModeState _mode; private readonly LocalPlayerModeState _mode;
private readonly LocalPlayerControllerSlot _controllerSlot; private readonly RuntimeLocalPlayerMovementState _controllerSlot;
private readonly LocalPlayerPhysicsHostSlot _hostSlot; private readonly LocalPlayerPhysicsHostSlot _hostSlot;
private readonly ChaseCameraInputState _chase; private readonly ChaseCameraInputState _chase;
private readonly CameraController _camera; private readonly CameraController _camera;
@ -49,7 +49,7 @@ internal sealed class PlayerModeController :
public PlayerModeController( public PlayerModeController(
LocalPlayerModeState mode, LocalPlayerModeState mode,
LocalPlayerControllerSlot controllerSlot, RuntimeLocalPlayerMovementState controllerSlot,
LocalPlayerPhysicsHostSlot hostSlot, LocalPlayerPhysicsHostSlot hostSlot,
ChaseCameraInputState chase, ChaseCameraInputState chase,
CameraController camera, CameraController camera,

View file

@ -35,7 +35,7 @@ namespace AcDream.App.Net;
internal sealed record LiveSessionPlayerRuntime( internal sealed record LiveSessionPlayerRuntime(
LocalPlayerIdentityState Identity, LocalPlayerIdentityState Identity,
LocalPlayerControllerSlot Controller, RuntimeLocalPlayerMovementState Controller,
LiveWorldOriginState WorldOrigin); LiveWorldOriginState WorldOrigin);
internal sealed record LiveSessionDomainRuntime( internal sealed record LiveSessionDomainRuntime(

View file

@ -41,7 +41,6 @@ internal sealed class LiveEntityNetworkUpdateController
private readonly ProjectileController _projectileController; private readonly ProjectileController _projectileController;
private readonly RemoteTeleportController _remoteTeleportController; private readonly RemoteTeleportController _remoteTeleportController;
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animatedEntities; private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animatedEntities;
private readonly LiveEntityRemoteMotionRuntimeView<RemoteMotion> _remoteDeadReckon;
private readonly RemoteMovementObservationTracker _remoteMovementObservations; private readonly RemoteMovementObservationTracker _remoteMovementObservations;
private readonly RemotePhysicsUpdater _remotePhysicsUpdater; private readonly RemotePhysicsUpdater _remotePhysicsUpdater;
private readonly RemoteInboundMotionDispatcher _remoteInboundMotion; private readonly RemoteInboundMotionDispatcher _remoteInboundMotion;
@ -53,7 +52,7 @@ internal sealed class LiveEntityNetworkUpdateController
private readonly LiveWorldOriginState _origin; private readonly LiveWorldOriginState _origin;
private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink
_localPlayerTeleport; _localPlayerTeleport;
private readonly ILocalPlayerControllerSource _playerControllerSource; private readonly IRuntimeLocalPlayerControllerSource _playerControllerSource;
private readonly LocalPlayerOutboundController _localPlayerOutbound; private readonly LocalPlayerOutboundController _localPlayerOutbound;
private readonly ILocalPlayerPhysicsHostSource _playerHostSource; private readonly ILocalPlayerPhysicsHostSource _playerHostSource;
private readonly ILocalPlayerIdentitySource _playerIdentity; private readonly ILocalPlayerIdentitySource _playerIdentity;
@ -83,7 +82,6 @@ internal sealed class LiveEntityNetworkUpdateController
ProjectileController projectileController, ProjectileController projectileController,
RemoteTeleportController remoteTeleportController, RemoteTeleportController remoteTeleportController,
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animatedEntities, LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animatedEntities,
LiveEntityRemoteMotionRuntimeView<RemoteMotion> remoteDeadReckon,
RemoteMovementObservationTracker remoteMovementObservations, RemoteMovementObservationTracker remoteMovementObservations,
RemotePhysicsUpdater remotePhysicsUpdater, RemotePhysicsUpdater remotePhysicsUpdater,
RemoteInboundMotionDispatcher remoteInboundMotion, RemoteInboundMotionDispatcher remoteInboundMotion,
@ -94,7 +92,7 @@ internal sealed class LiveEntityNetworkUpdateController
RuntimeCombatTargetState? combatTargetController, RuntimeCombatTargetState? combatTargetController,
LiveWorldOriginState origin, LiveWorldOriginState origin,
AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport, AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport,
ILocalPlayerControllerSource playerControllerSource, IRuntimeLocalPlayerControllerSource playerControllerSource,
LocalPlayerOutboundController localPlayerOutbound, LocalPlayerOutboundController localPlayerOutbound,
ILocalPlayerPhysicsHostSource playerHostSource, ILocalPlayerPhysicsHostSource playerHostSource,
ILocalPlayerIdentitySource playerIdentity, ILocalPlayerIdentitySource playerIdentity,
@ -113,7 +111,6 @@ internal sealed class LiveEntityNetworkUpdateController
_projectileController = projectileController ?? throw new ArgumentNullException(nameof(projectileController)); _projectileController = projectileController ?? throw new ArgumentNullException(nameof(projectileController));
_remoteTeleportController = remoteTeleportController ?? throw new ArgumentNullException(nameof(remoteTeleportController)); _remoteTeleportController = remoteTeleportController ?? throw new ArgumentNullException(nameof(remoteTeleportController));
_animatedEntities = animatedEntities ?? throw new ArgumentNullException(nameof(animatedEntities)); _animatedEntities = animatedEntities ?? throw new ArgumentNullException(nameof(animatedEntities));
_remoteDeadReckon = remoteDeadReckon ?? throw new ArgumentNullException(nameof(remoteDeadReckon));
_remoteMovementObservations = remoteMovementObservations ?? throw new ArgumentNullException(nameof(remoteMovementObservations)); _remoteMovementObservations = remoteMovementObservations ?? throw new ArgumentNullException(nameof(remoteMovementObservations));
_remotePhysicsUpdater = remotePhysicsUpdater ?? throw new ArgumentNullException(nameof(remotePhysicsUpdater)); _remotePhysicsUpdater = remotePhysicsUpdater ?? throw new ArgumentNullException(nameof(remotePhysicsUpdater));
_remoteInboundMotion = remoteInboundMotion ?? throw new ArgumentNullException(nameof(remoteInboundMotion)); _remoteInboundMotion = remoteInboundMotion ?? throw new ArgumentNullException(nameof(remoteInboundMotion));
@ -151,7 +148,10 @@ internal sealed class LiveEntityNetworkUpdateController
uint localEntityId, uint localEntityId,
Func<bool> isCurrent) Func<bool> isCurrent)
{ {
_remoteDeadReckon.TryGetValue(serverGuid, out RemoteMotion? remote); _liveEntities.TryGetRemoteMotionRuntime(
serverGuid,
out IRuntimeRemoteMotion? remoteRuntime);
RemoteMotion? remote = remoteRuntime as RemoteMotion;
EntityPhysicsHost? host = EntityPhysicsHost? host =
_liveEntities.TryGetPhysicsHost(serverGuid, out var registered) _liveEntities.TryGetPhysicsHost(serverGuid, out var registered)
? registered as EntityPhysicsHost ? registered as EntityPhysicsHost
@ -570,7 +570,10 @@ internal sealed class LiveEntityNetworkUpdateController
_remoteMovementObservations[motionKey] = _remoteMovementObservations[motionKey] =
(prev.Pos, refreshedTime); (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; dr.LastServerPosTime = (refreshedTime - System.DateTime.UnixEpoch).TotalSeconds;
} }
@ -635,12 +638,15 @@ internal sealed class LiveEntityNetworkUpdateController
if (!IsCurrentOwner()) if (!IsCurrentOwner())
return default; 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.Orientation = entity.Rotation;
remote.Body.Position = entity.Position; remote.Body.Position = entity.Position;
_remoteDeadReckon[update.Guid] = remote;
} }
if (!IsCurrentOwner(remote)) if (!IsCurrentOwner(remote))
return default; return default;
@ -719,43 +725,6 @@ internal sealed class LiveEntityNetworkUpdateController
&& runtime.IsCurrentSpatialRemoteMotion(record, remote); && runtime.IsCurrentSpatialRemoteMotion(record, remote);
} }
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary> /// <summary>
/// K-fix9 (2026-04-26): handle 0xF74E VectorUpdate from remote jumps. /// K-fix9 (2026-04-26): handle 0xF74E VectorUpdate from remote jumps.
/// The payload seeds the world-space launch velocity and angular velocity. /// 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 (!_liveEntities.ContainsWorldEntity(update.Guid)) return;
if (update.Guid == _playerServerGuid) return; // local jump uses our own physics 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; LiveEntityRecord remoteRecord = acceptedVectorRecord;
// World-space velocity. Apply directly to the body — the per-tick // 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 // small. When HasVelocity is set, we also seed PhysicsBody
// velocity (matches retail's set_velocity call in the same // velocity (matches retail's set_velocity call in the same
// dispatcher). // 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); rmState =
_remoteDeadReckon[update.Guid] = rmState; _liveEntities.GetOrCreateRemoteMotionRuntime(
update.Guid);
// Hard-snap orientation on first spawn so the per-tick // Hard-snap orientation on first spawn so the per-tick
// slerp doesn't visibly rotate from Identity to truth. // slerp doesn't visibly rotate from Identity to truth.
rmState.Body.Orientation = rot; rmState.Body.Orientation = rot;

View file

@ -14,7 +14,7 @@ internal static class LiveEntityShadowPublisher
LiveEntityRuntime runtime, LiveEntityRuntime runtime,
LiveEntityRecord record, LiveEntityRecord record,
WorldEntity entity, WorldEntity entity,
ILiveEntityRemoteMotionRuntime remote, IRuntimeRemoteMotion remote,
ulong positionAuthorityVersion, ulong positionAuthorityVersion,
Action publish) Action publish)
{ {
@ -47,7 +47,7 @@ internal static class LiveEntityShadowPublisher
LiveEntityRuntime runtime, LiveEntityRuntime runtime,
LiveEntityRecord record, LiveEntityRecord record,
WorldEntity entity, WorldEntity entity,
ILiveEntityRemoteMotionRuntime remote, IRuntimeRemoteMotion remote,
ulong positionAuthorityVersion) => ulong positionAuthorityVersion) =>
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) == 0 (record.FinalPhysicsState & PhysicsStateFlags.Hidden) == 0
&& ReferenceEquals(record.WorldEntity, entity) && ReferenceEquals(record.WorldEntity, entity)

View file

@ -246,7 +246,6 @@ internal sealed class ProjectileController
{ {
var created = new PhysicsBody var created = new PhysicsBody
{ {
State = record.FinalPhysicsState,
Friction = NormalizeFriction( Friction = NormalizeFriction(
physics.Friction ?? record.Snapshot.Friction), physics.Friction ?? record.Snapshot.Friction),
Elasticity = NormalizeElasticity( Elasticity = NormalizeElasticity(
@ -254,8 +253,6 @@ internal sealed class ProjectileController
Orientation = entity.Rotation, Orientation = entity.Rotation,
LastUpdateTime = currentTime, LastUpdateTime = currentTime,
}; };
SetVelocity(created, velocity, currentTime);
created.Omega = omega;
created.SnapToCell( created.SnapToCell(
wirePosition.LandblockId, wirePosition.LandblockId,
entity.Position, entity.Position,
@ -919,22 +916,6 @@ internal sealed class ProjectileController
private static void Deactivate(PhysicsBody body) => private static void Deactivate(PhysicsBody body) =>
body.TransientState &= ~TransientStateFlags.Active; 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) => private static float NormalizeFriction(float? value) =>
value is >= 0f and <= 1f && float.IsFinite(value.Value) value is >= 0f and <= 1f && float.IsFinite(value.Value)
? value.Value ? value.Value

View file

@ -1,39 +0,0 @@
using System.Numerics;
using AcDream.App.World;
using AcDream.Core.Physics;
namespace AcDream.App.Physics;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Retail <c>CPhysicsObj::set_description</c> <c>0x00514F40</c> installs the
/// unpacked velocity/omega during object initialization. Retail
/// <c>SmartBox::DoSetState</c> <c>0x004520D0</c> only calls
/// <c>CPhysicsObj::set_state</c> and therefore preserves the current vectors.
/// </remarks>
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);
}

View file

@ -73,7 +73,7 @@ internal sealed class RemoteTeleportController : IDisposable
LiveEntityRecord Record, LiveEntityRecord Record,
ulong PositionAuthorityVersion, ulong PositionAuthorityVersion,
ulong VelocityAuthorityVersion, ulong VelocityAuthorityVersion,
ILiveEntityRemotePlacementRuntime Remote, IRuntimeRemotePlacement Remote,
PhysicsBody Body, PhysicsBody Body,
WorldEntity Entity, WorldEntity Entity,
Vector3 RequestedWorldPosition, Vector3 RequestedWorldPosition,
@ -110,7 +110,7 @@ internal sealed class RemoteTeleportController : IDisposable
/// revalidation once admitted. /// revalidation once admitted.
/// </summary> /// </summary>
internal Result TryApply( internal Result TryApply(
ILiveEntityRemotePlacementRuntime remote, IRuntimeRemotePlacement remote,
WorldEntity entity, WorldEntity entity,
Vector3 requestedWorldPosition, Vector3 requestedWorldPosition,
uint requestedCellId, uint requestedCellId,
@ -147,7 +147,7 @@ internal sealed class RemoteTeleportController : IDisposable
LiveEntityRecord expectedRecord, LiveEntityRecord expectedRecord,
ulong expectedPositionAuthorityVersion, ulong expectedPositionAuthorityVersion,
ulong expectedVelocityAuthorityVersion, ulong expectedVelocityAuthorityVersion,
ILiveEntityRemotePlacementRuntime remote, IRuntimeRemotePlacement remote,
WorldEntity entity, WorldEntity entity,
Vector3 requestedWorldPosition, Vector3 requestedWorldPosition,
uint requestedCellId, uint requestedCellId,
@ -422,7 +422,7 @@ internal sealed class RemoteTeleportController : IDisposable
_pending.Remove(key); _pending.Remove(key);
return; return;
} }
if (record.RemoteMotionRuntime is not ILiveEntityRemotePlacementRuntime currentRemote if (record.RemoteMotionRuntime is not IRuntimeRemotePlacement currentRemote
|| record.PhysicsBody is null || record.PhysicsBody is null
|| !ReferenceEquals(record.PhysicsBody, pending.Body) || !ReferenceEquals(record.PhysicsBody, pending.Body)
|| !ReferenceEquals(currentRemote.Body, record.PhysicsBody)) || !ReferenceEquals(currentRemote.Body, record.PhysicsBody))
@ -467,7 +467,7 @@ internal sealed class RemoteTeleportController : IDisposable
} }
private static RollbackPlacement CaptureRollback( private static RollbackPlacement CaptureRollback(
ILiveEntityRemotePlacementRuntime remote, IRuntimeRemotePlacement remote,
PhysicsBody body) PhysicsBody body)
{ {
return new RollbackPlacement( return new RollbackPlacement(

View file

@ -13,7 +13,7 @@ namespace AcDream.App.Physics;
internal static class RemoteTeleportPlacement internal static class RemoteTeleportPlacement
{ {
internal static bool Apply( internal static bool Apply(
ILiveEntityRemotePlacementRuntime remote, IRuntimeRemotePlacement remote,
PhysicsBody body, PhysicsBody body,
ResolveResult placement, ResolveResult placement,
Vector3 cellLocalPosition, Vector3 cellLocalPosition,

View file

@ -1005,7 +1005,6 @@ internal sealed class DatLiveEntityProjectionMaterializer
incarnation => incarnation =>
{ {
var created = new PhysicsBody { Orientation = entity.Rotation }; var created = new PhysicsBody { Orientation = entity.Rotation };
RemotePhysicsBodyInitializer.Initialize(created, incarnation);
created.SnapToCell( created.SnapToCell(
staticPosition.LandblockId, staticPosition.LandblockId,
entity.Position, entity.Position,

View file

@ -257,7 +257,7 @@ internal interface IDevToolsRuntimeFacts
internal sealed class DevToolsRuntimeFacts : IDevToolsRuntimeFacts internal sealed class DevToolsRuntimeFacts : IDevToolsRuntimeFacts
{ {
private readonly ILocalPlayerModeSource _mode; private readonly ILocalPlayerModeSource _mode;
private readonly ILocalPlayerControllerSource _player; private readonly IRuntimeLocalPlayerControllerSource _player;
private readonly CameraController _camera; private readonly CameraController _camera;
private readonly ICanonicalWorldEntityCountSource _world; private readonly ICanonicalWorldEntityCountSource _world;
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animations; private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animations;
@ -273,7 +273,7 @@ internal sealed class DevToolsRuntimeFacts : IDevToolsRuntimeFacts
public DevToolsRuntimeFacts( public DevToolsRuntimeFacts(
ILocalPlayerModeSource mode, ILocalPlayerModeSource mode,
ILocalPlayerControllerSource player, IRuntimeLocalPlayerControllerSource player,
CameraController camera, CameraController camera,
ICanonicalWorldEntityCountSource world, ICanonicalWorldEntityCountSource world,
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animations, LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animations,

View file

@ -218,7 +218,7 @@ internal sealed class RuntimeWorldFrameRootSource : IWorldFrameRootSource
private readonly CellVisibility _cells; private readonly CellVisibility _cells;
private readonly ILocalPlayerModeSource _mode; private readonly ILocalPlayerModeSource _mode;
private readonly IChaseCameraSource _chase; private readonly IChaseCameraSource _chase;
private readonly ILocalPlayerControllerSource _player; private readonly IRuntimeLocalPlayerControllerSource _player;
private readonly LiveWorldOriginState _origin; private readonly LiveWorldOriginState _origin;
public RuntimeWorldFrameRootSource( public RuntimeWorldFrameRootSource(
@ -226,7 +226,7 @@ internal sealed class RuntimeWorldFrameRootSource : IWorldFrameRootSource
CellVisibility cells, CellVisibility cells,
ILocalPlayerModeSource mode, ILocalPlayerModeSource mode,
IChaseCameraSource chase, IChaseCameraSource chase,
ILocalPlayerControllerSource player, IRuntimeLocalPlayerControllerSource player,
LiveWorldOriginState origin) LiveWorldOriginState origin)
{ {
_physics = physics ?? throw new ArgumentNullException(nameof(physics)); _physics = physics ?? throw new ArgumentNullException(nameof(physics));

View file

@ -60,7 +60,7 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
private readonly DebugLineRenderer? _lines; private readonly DebugLineRenderer? _lines;
private readonly PhysicsEngine _physics; private readonly PhysicsEngine _physics;
private readonly ILocalPlayerModeSource _mode; private readonly ILocalPlayerModeSource _mode;
private readonly ILocalPlayerControllerSource _player; private readonly IRuntimeLocalPlayerControllerSource _player;
private readonly DebugVmRenderFactsPublisher _debugVm; private readonly DebugVmRenderFactsPublisher _debugVm;
private readonly bool _debugVmConsumerActive; private readonly bool _debugVmConsumerActive;
private int _debugDrawLogCount; private int _debugDrawLogCount;
@ -72,7 +72,7 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
DebugLineRenderer? lines, DebugLineRenderer? lines,
PhysicsEngine physics, PhysicsEngine physics,
ILocalPlayerModeSource mode, ILocalPlayerModeSource mode,
ILocalPlayerControllerSource player, IRuntimeLocalPlayerControllerSource player,
DebugVmRenderFactsPublisher debugVm, DebugVmRenderFactsPublisher debugVm,
bool debugVmConsumerActive) bool debugVmConsumerActive)
{ {

View file

@ -55,12 +55,12 @@ internal interface IWorldScenePViewDiagnosticSource
internal sealed class RuntimeWorldScenePViewDiagnosticSource : internal sealed class RuntimeWorldScenePViewDiagnosticSource :
IWorldScenePViewDiagnosticSource IWorldScenePViewDiagnosticSource
{ {
private readonly ILocalPlayerControllerSource _player; private readonly IRuntimeLocalPlayerControllerSource _player;
private readonly PhysicsEngine _physics; private readonly PhysicsEngine _physics;
private readonly CellVisibility _visibility; private readonly CellVisibility _visibility;
public RuntimeWorldScenePViewDiagnosticSource( public RuntimeWorldScenePViewDiagnosticSource(
ILocalPlayerControllerSource player, IRuntimeLocalPlayerControllerSource player,
PhysicsEngine physics, PhysicsEngine physics,
CellVisibility visibility) CellVisibility visibility)
{ {

View file

@ -32,7 +32,6 @@ internal sealed class CurrentGameRuntimeAdapter
LiveSessionHost sessionHost, LiveSessionHost sessionHost,
ICommandBus commands, ICommandBus commands,
LocalPlayerIdentityState playerIdentity, LocalPlayerIdentityState playerIdentity,
LiveEntityRuntime entities,
RuntimeEntityObjectLifetime entityObjects, RuntimeEntityObjectLifetime entityObjects,
RuntimeInventoryState inventory, RuntimeInventoryState inventory,
RuntimeCharacterState character, RuntimeCharacterState character,
@ -46,7 +45,6 @@ internal sealed class CurrentGameRuntimeAdapter
_view = new CurrentGameRuntimeViewAdapter( _view = new CurrentGameRuntimeViewAdapter(
session, session,
playerIdentity, playerIdentity,
entities,
entityObjects, entityObjects,
inventory, inventory,
character, character,

View file

@ -1,7 +1,6 @@
using AcDream.App.Input; using AcDream.App.Input;
using AcDream.App.Net; using AcDream.App.Net;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Chat; using AcDream.Core.Chat;
using AcDream.Core.Items; using AcDream.Core.Items;
using AcDream.Runtime; using AcDream.Runtime;
@ -19,8 +18,6 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
{ {
private readonly LiveSessionController _session; private readonly LiveSessionController _session;
private readonly LocalPlayerIdentityState _playerIdentity; private readonly LocalPlayerIdentityState _playerIdentity;
private readonly LiveEntityRuntime _entities;
private readonly ClientObjectTable _objects;
private readonly IGameRuntimeClock _clock; private readonly IGameRuntimeClock _clock;
private readonly IRuntimeEntityView _entityView; private readonly IRuntimeEntityView _entityView;
private readonly IRuntimeInventoryView _inventoryView; private readonly IRuntimeInventoryView _inventoryView;
@ -36,7 +33,6 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
public CurrentGameRuntimeViewAdapter( public CurrentGameRuntimeViewAdapter(
LiveSessionController session, LiveSessionController session,
LocalPlayerIdentityState playerIdentity, LocalPlayerIdentityState playerIdentity,
LiveEntityRuntime entities,
RuntimeEntityObjectLifetime entityObjects, RuntimeEntityObjectLifetime entityObjects,
RuntimeInventoryState inventory, RuntimeInventoryState inventory,
RuntimeCharacterState character, RuntimeCharacterState character,
@ -49,9 +45,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
_session = session ?? throw new ArgumentNullException(nameof(session)); _session = session ?? throw new ArgumentNullException(nameof(session));
_playerIdentity = playerIdentity _playerIdentity = playerIdentity
?? throw new ArgumentNullException(nameof(playerIdentity)); ?? throw new ArgumentNullException(nameof(playerIdentity));
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
ArgumentNullException.ThrowIfNull(entityObjects); ArgumentNullException.ThrowIfNull(entityObjects);
_objects = entityObjects.Objects;
ArgumentNullException.ThrowIfNull(communication); ArgumentNullException.ThrowIfNull(communication);
_clock = clock ?? throw new ArgumentNullException(nameof(clock)); _clock = clock ?? throw new ArgumentNullException(nameof(clock));
@ -115,10 +109,10 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
Generation, Generation,
Lifecycle.State, Lifecycle.State,
_clock.FrameNumber, _clock.FrameNumber,
_entities.Count, _entityView.Count,
_entities.MaterializedCount, _entityView.MaterializedCount,
_objects.ObjectCount, _inventoryView.ObjectCount,
_objects.ContainerCount, _inventoryView.ContainerCount,
_inventoryStateView.Snapshot, _inventoryStateView.Snapshot,
_characterView.Snapshot, _characterView.Snapshot,
_socialView.Snapshot, _socialView.Snapshot,

View file

@ -179,7 +179,7 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
private readonly PhysicsEngine _physics; private readonly PhysicsEngine _physics;
private readonly LiveEntityRuntime _liveEntities; private readonly LiveEntityRuntime _liveEntities;
private readonly ILocalPlayerIdentitySource _identity; private readonly ILocalPlayerIdentitySource _identity;
private readonly ILocalPlayerControllerSource _controller; private readonly IRuntimeLocalPlayerControllerSource _controller;
private readonly ILocalPlayerPhysicsHostSource _host; private readonly ILocalPlayerPhysicsHostSource _host;
private readonly ChaseCameraInputState _cameras; private readonly ChaseCameraInputState _cameras;
private readonly LiveWorldOriginState _origin; private readonly LiveWorldOriginState _origin;
@ -189,7 +189,7 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
PhysicsEngine physics, PhysicsEngine physics,
LiveEntityRuntime liveEntities, LiveEntityRuntime liveEntities,
ILocalPlayerIdentitySource identity, ILocalPlayerIdentitySource identity,
ILocalPlayerControllerSource controller, IRuntimeLocalPlayerControllerSource controller,
ILocalPlayerPhysicsHostSource host, ILocalPlayerPhysicsHostSource host,
ChaseCameraInputState cameras, ChaseCameraInputState cameras,
LiveWorldOriginState origin, LiveWorldOriginState origin,

View file

@ -106,7 +106,7 @@ internal sealed class StreamingFrameController : IStreamingFramePhase
private readonly bool _liveModeEnabled; private readonly bool _liveModeEnabled;
private readonly ILocalPlayerModeSource _playerMode; private readonly ILocalPlayerModeSource _playerMode;
private readonly ILocalPlayerControllerSource _playerController; private readonly IRuntimeLocalPlayerControllerSource _playerController;
private readonly ILiveInWorldSource _session; private readonly ILiveInWorldSource _session;
private readonly LiveWorldOriginState _origin; private readonly LiveWorldOriginState _origin;
private readonly ILocalPlayerLandblockSource _playerLandblock; private readonly ILocalPlayerLandblockSource _playerLandblock;
@ -119,7 +119,7 @@ internal sealed class StreamingFrameController : IStreamingFramePhase
public StreamingFrameController( public StreamingFrameController(
bool liveModeEnabled, bool liveModeEnabled,
ILocalPlayerModeSource playerMode, ILocalPlayerModeSource playerMode,
ILocalPlayerControllerSource playerController, IRuntimeLocalPlayerControllerSource playerController,
ILiveInWorldSource session, ILiveInWorldSource session,
LiveWorldOriginState origin, LiveWorldOriginState origin,
ILocalPlayerLandblockSource playerLandblock, ILocalPlayerLandblockSource playerLandblock,

View file

@ -223,33 +223,27 @@ public sealed class LiveEntityRecord
public PhysicsBody? PhysicsBody public PhysicsBody? PhysicsBody
{ {
get => Canonical.PhysicsBody; get => Canonical.PhysicsBody;
internal set => _directory.SetPhysicsBody(Canonical, value);
} }
internal bool PhysicsBodyAcquisitionInProgress internal bool PhysicsBodyAcquisitionInProgress
{ {
get => Canonical.PhysicsBodyAcquisitionInProgress; get => Canonical.PhysicsBodyAcquisitionInProgress;
set => _directory.SetPhysicsBodyAcquisitionInProgress(Canonical, value);
} }
public ILiveEntityAnimationRuntime? AnimationRuntime { get; internal set; } public ILiveEntityAnimationRuntime? AnimationRuntime { get; internal set; }
public ILiveEntityRemoteMotionRuntime? RemoteMotionRuntime public IRuntimeRemoteMotion? RemoteMotionRuntime
{ {
get => Canonical.RemoteMotion; get => Canonical.RemoteMotion;
internal set => _directory.SetRemoteMotion(Canonical, value);
} }
internal bool RemoteMotionBindingInProgress internal bool RemoteMotionBindingInProgress
{ {
get => Canonical.RemoteMotionBindingInProgress; get => Canonical.RemoteMotionBindingInProgress;
set => _directory.SetRemoteMotionBindingInProgress(Canonical, value);
} }
public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost
{ {
get => Canonical.PhysicsHost; get => Canonical.PhysicsHost;
internal set => _directory.SetPhysicsHost(Canonical, value);
} }
internal bool RequiresRemotePlacementRuntime internal bool RequiresRemotePlacementRuntime
{ {
get => Canonical.RequiresRemotePlacementRuntime; get => Canonical.RequiresRemotePlacementRuntime;
set => _directory.SetRequiresRemotePlacementRuntime(Canonical, value);
} }
public IRuntimeProjectile? ProjectileRuntime => Canonical.Projectile; public IRuntimeProjectile? ProjectileRuntime => Canonical.Projectile;
public ILiveEntityEffectProfile? EffectProfile { get; internal set; } public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
@ -1389,7 +1383,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
ProjectionIsCurrent); ProjectionIsCurrent);
} }
public void SetRemoteMotionRuntime(uint serverGuid, ILiveEntityRemoteMotionRuntime runtime) internal void SetRemoteMotionRuntime(uint serverGuid, IRuntimeRemoteMotion runtime)
{ {
ArgumentNullException.ThrowIfNull(runtime); ArgumentNullException.ThrowIfNull(runtime);
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)) if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record))
@ -1442,7 +1436,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
public bool TryGetRemoteMotionRuntime( public bool TryGetRemoteMotionRuntime(
uint serverGuid, uint serverGuid,
out ILiveEntityRemoteMotionRuntime runtime) out IRuntimeRemoteMotion runtime)
{ {
if (_projections.TryGetCurrent( if (_projections.TryGetCurrent(
serverGuid, serverGuid,
@ -1457,7 +1451,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
return false; return false;
} }
public bool ClearRemoteMotionRuntime(uint serverGuid) internal bool ClearRemoteMotionRuntime(uint serverGuid)
{ {
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record) if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|| record.RemoteMotionRuntime is null) || record.RemoteMotionRuntime is null)
@ -1468,6 +1462,26 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
return true; 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( internal IRuntimeProjectile BindProjectileRuntime(
uint serverGuid, uint serverGuid,
PhysicsBody body, PhysicsBody body,
@ -1874,27 +1888,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
destination.Add(key.LocalEntityId); destination.Add(key.LocalEntityId);
} }
/// <summary>
/// Host side of retail <c>CPhysicsObj::set_active(1)</c> for a movement
/// manager owned by this exact incarnation. Static is a required no-op.
/// </summary>
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;
}
/// <summary> /// <summary>
/// Commits retail <c>CPhysicsObj::set_velocity</c> (0x005113F0) to the /// Commits retail <c>CPhysicsObj::set_velocity</c> (0x005113F0) to the
/// canonical body for this exact incarnation. The body and the retained /// 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(record);
ArgumentNullException.ThrowIfNull(body); ArgumentNullException.ThrowIfNull(body);
if (!IsFinite(velocity) bool ProjectionIsCurrent() =>
|| (angularVelocity is { } omega && !IsFinite(omega)) _projections.TryGetCurrent(
|| !double.IsFinite(currentTime)
|| !_projections.TryGetCurrent(
record.ServerGuid, record.ServerGuid,
out LiveEntityRecord? current) out LiveEntityRecord? current)
|| !ReferenceEquals(current, record) && ReferenceEquals(current, record)
|| !ReferenceEquals(current.PhysicsBody, body)) && ReferenceEquals(current.PhysicsBody, body);
{ return _physics.TryCommitAuthoritativeVector(
return false; record.Canonical,
} body,
velocity,
bool wasBodyActive = angularVelocity,
(body.TransientState & TransientStateFlags.Active) != 0; currentTime,
body.set_velocity(velocity); ProjectionIsCurrent);
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;
} }
/// <summary> /// <summary>
@ -2007,7 +1978,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
internal bool IsCurrentSpatialRemoteMotion( internal bool IsCurrentSpatialRemoteMotion(
LiveEntityRecord record, LiveEntityRecord record,
ILiveEntityRemoteMotionRuntime runtime) => IRuntimeRemoteMotion runtime) =>
IsCurrentRecord(record) IsCurrentRecord(record)
&& _physics.IsSpatialRemote(record.Canonical, runtime) && _physics.IsSpatialRemote(record.Canonical, runtime)
&& HasSpatialRuntimeProjection(record); && HasSpatialRuntimeProjection(record);
@ -2546,11 +2517,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
body.TransientState &= ~TransientStateFlags.Active; 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) private void PublishProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
{ {
Delegate[] subscribers = ProjectionVisibilityChanged?.GetInvocationList() Delegate[] subscribers = ProjectionVisibilityChanged?.GetInvocationList()
@ -2728,16 +2694,11 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
} }
if (record.WorldEntity is not null) if (record.WorldEntity is not null)
{
_ = RequireProjectionKey(record); _ = RequireProjectionKey(record);
_directory.ReleaseLocalId(record.Canonical); _entityObjects.CompleteProjectionRetirement(
} record.Canonical);
record.AnimationRuntime = null; record.AnimationRuntime = null;
record.RemoteMotionRuntime = null;
record.PhysicsHost = null;
record.RequiresRemotePlacementRuntime = false;
record.PhysicsBody = null;
record.EffectProfile = null; record.EffectProfile = null;
record.IsSpatiallyProjected = false; record.IsSpatiallyProjected = false;
record.IsSpatiallyVisible = false; record.IsSpatiallyVisible = false;

View file

@ -133,36 +133,3 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
throw new NotSupportedException(); throw new NotSupportedException();
} }
} }
/// <summary>Storage-free typed view over remote-motion components.</summary>
public sealed class LiveEntityRemoteMotionRuntimeView<TRemoteMotion>
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;
}

View file

@ -168,6 +168,27 @@ public sealed class PhysicsEngine
DataCache?.CellGraph.RemoveLandblock(landblockId); DataCache?.CellGraph.RemoveLandblock(landblockId);
} }
/// <summary>
/// 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.
/// </summary>
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();
}
/// <summary> /// <summary>
/// Retire a Near landblock's indoor-cell and static-object collision layer /// Retire a Near landblock's indoor-cell and static-object collision layer
/// while preserving its terrain surface and world offset for Far-tier use. /// while preserving its terrain surface and world offset for Far-tier use.

View file

@ -795,6 +795,21 @@ public sealed class ShadowObjectRegistry
/// <summary>Suspended logical registrations awaiting spatial re-entry.</summary> /// <summary>Suspended logical registrations awaiting spatial re-entry.</summary>
public int SuspendedRegistrationCount => _suspendedEntities.Count; public int SuspendedRegistrationCount => _suspendedEntities.Count;
/// <summary>
/// Retires the complete logical registry at terminal physics-engine
/// disposal, including suspended live registrations that own no cell row.
/// </summary>
public void Clear()
{
_cells.Clear();
_entityToCells.Clear();
_suspendedEntities.Clear();
_withdrawnPrefixesByOwner.Clear();
_entityShapes.Clear();
_entityReg.Clear();
_fallback = null;
}
/// <summary> /// <summary>
/// Debug: enumerate every registered ShadowEntry (deduplicated across cells). /// Debug: enumerate every registered ShadowEntry (deduplicated across cells).
/// Single-shape entities return one entry per shape; multi-part entities /// Single-shape entities return one entry per shape; multi-part entities

View file

@ -32,7 +32,29 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
bool HasLastStreamDispatchFailure, bool HasLastStreamDispatchFailure,
int PendingDispatchCount, int PendingDispatchCount,
bool IsDispatching, 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;
}
/// <summary> /// <summary>
/// One exact DeleteObject acceptance issued by a /// One exact DeleteObject acceptance issued by a
@ -140,7 +162,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Events.LastDispatchFailure is not null, Events.LastDispatchFailure is not null,
Events.PendingDispatchCount, Events.PendingDispatchCount,
Events.IsDispatching, Events.IsDispatching,
_sessionClearInProgress); _sessionClearInProgress,
_disposed);
} }
public void BindEventContext( public void BindEventContext(
@ -355,17 +378,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.RetainTeardown(canonical); Entities.RetainTeardown(canonical);
try try
{ {
Physics.RemoveSpatialProjection(canonical); CompleteProjectionRetirement(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);
} }
finally finally
{ {
@ -379,6 +392,30 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
} }
} }
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary> /// <summary>
/// Applies the retained-object half of an accepted CreateObject while the /// Applies the retained-object half of an accepted CreateObject while the
/// exact canonical incarnation and its integration version remain current. /// exact canonical incarnation and its integration version remain current.

View file

@ -85,6 +85,7 @@ internal sealed class RuntimeEntityObjectViews
: IRuntimeEntityView : IRuntimeEntityView
{ {
public int Count => owner.Count; public int Count => owner.Count;
public int MaterializedCount => owner.ClaimedLocalIdCount;
public bool TryGet( public bool TryGet(
uint serverGuid, uint serverGuid,

View file

@ -22,6 +22,8 @@ public interface IRuntimeEntityView
{ {
int Count { get; } int Count { get; }
int MaterializedCount { get; }
bool TryGet(uint serverGuid, out RuntimeEntitySnapshot entity); bool TryGet(uint serverGuid, out RuntimeEntitySnapshot entity);
/// <summary> /// <summary>

View file

@ -12,7 +12,19 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
int CollisionAdmissionCount, int CollisionAdmissionCount,
int CollisionGenerationCount, int CollisionGenerationCount,
bool OwnsProductionDataCache, 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( public readonly record struct RuntimePhysicsCellCommit(
RuntimeEntityRecord Record, RuntimeEntityRecord Record,
@ -437,6 +449,8 @@ public sealed class RuntimePhysicsState : IDisposable
record, record,
expectedPlacementContract expectedPlacementContract
|| runtime is IRuntimeRemotePlacement); || runtime is IRuntimeRemotePlacement);
if (expectedBody is null)
InitializeNewPhysicsBody(record, candidateBody);
Entities.SetRemoteMotion(record, runtime); Entities.SetRemoteMotion(record, runtime);
Entities.SetPhysicsBody(record, candidateBody); Entities.SetPhysicsBody(record, candidateBody);
candidateBody.State = record.FinalPhysicsState; candidateBody.State = record.FinalPhysicsState;
@ -452,6 +466,52 @@ public sealed class RuntimePhysicsState : IDisposable
} }
} }
/// <summary>
/// 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.
/// </summary>
internal RemoteMotion GetOrCreateRemoteMotion(
RuntimeEntityRecord record,
Func<bool>? 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<bool>? 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;
}
/// <summary> /// <summary>
/// Acquires the one retail <c>CPhysicsObj</c> body for an exact accepted /// Acquires the one retail <c>CPhysicsObj</c> body for an exact accepted
/// object incarnation. Factories may cross a host boundary, so ownership /// 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."); $"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} acquired two physics bodies within one incarnation.");
} }
InitializeNewPhysicsBody(record, candidate);
Entities.SetPhysicsBody(record, candidate); Entities.SetPhysicsBody(record, candidate);
candidate.State = record.FinalPhysicsState; candidate.State = record.FinalPhysicsState;
SynchronizeBodyActiveState(record); SynchronizeBodyActiveState(record);
@ -905,6 +966,7 @@ public sealed class RuntimePhysicsState : IDisposable
{ {
if (_disposed) if (_disposed)
return; return;
Engine.Clear();
_spatialRemotes.Clear(); _spatialRemotes.Clear();
_spatialProjectiles.Clear(); _spatialProjectiles.Clear();
_spatialRoots.Clear(); _spatialRoots.Clear();
@ -1039,4 +1101,22 @@ public sealed class RuntimePhysicsState : IDisposable
else else
body.TransientState &= ~TransientStateFlags.Active; body.TransientState &= ~TransientStateFlags.Active;
} }
/// <summary>
/// Retail <c>CPhysicsObj::set_description</c> (0x00514F40) installs the
/// accepted CreateObject velocity and omega once, when the incarnation's
/// CPhysicsObj is first acquired. Later SetState never replays them.
/// </summary>
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;
}
} }

View file

@ -0,0 +1,44 @@
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime;
/// <summary>
/// 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.
/// </summary>
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));
}
}

View file

@ -96,7 +96,7 @@ public sealed class LiveCombatAttackOperationsTests
var combat = new CombatState(); var combat = new CombatState();
var targets = new FakeTargets(); var targets = new FakeTargets();
var settings = new FakeSettings(); var settings = new FakeSettings();
var player = new LocalPlayerControllerSlot(); var player = new RuntimeLocalPlayerMovementState();
var live = new FakeLiveSource { IsInWorld = inWorld }; var live = new FakeLiveSource { IsInWorld = inWorld };
var feedback = new FakeFeedback(); var feedback = new FakeFeedback();
var outbound = new LocalPlayerOutboundController( var outbound = new LocalPlayerOutboundController(

View file

@ -1,18 +1,4 @@
global using AcDream.Runtime.Gameplay; global using AcDream.Runtime.Gameplay;
global using AcDream.Runtime.Physics; 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 = global using ILocalPlayerMotionSource =
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource; AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource;

View file

@ -201,7 +201,7 @@ public sealed class MouseLookControllerTests
{ {
PlayerMovementController player = CreatePlayer(); PlayerMovementController player = CreatePlayer();
var mode = new LocalPlayerModeState(); 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 camera = new CameraController(new OrbitCamera(), new FlyCamera());
var chase = new ChaseCameraInputState(); var chase = new ChaseCameraInputState();
var mouse = new FakeMouse(); var mouse = new FakeMouse();

View file

@ -3,12 +3,12 @@ using AcDream.Core.Physics;
namespace AcDream.App.Tests.Input; namespace AcDream.App.Tests.Input;
public sealed class LocalPlayerControllerSlotTests public sealed class RuntimeLocalPlayerMovementStateTests
{ {
[Fact] [Fact]
public void MotionPreparation_RoutesCompletionWithoutPublishingController() public void MotionPreparation_RoutesCompletionWithoutPublishingController()
{ {
var slot = new LocalPlayerControllerSlot(); var slot = new RuntimeLocalPlayerMovementState();
ILocalPlayerMotionSource source = slot; ILocalPlayerMotionSource source = slot;
var controller = new PlayerMovementController(new PhysicsEngine()); var controller = new PlayerMovementController(new PhysicsEngine());
controller.Motion.AddToQueue(0, MotionCommand.Ready, 0); controller.Motion.AddToQueue(0, MotionCommand.Ready, 0);
@ -29,7 +29,7 @@ public sealed class LocalPlayerControllerSlotTests
[Fact] [Fact]
public void MotionPreparation_HandsOffToCommittedControllerOnDispose() public void MotionPreparation_HandsOffToCommittedControllerOnDispose()
{ {
var slot = new LocalPlayerControllerSlot(); var slot = new RuntimeLocalPlayerMovementState();
ILocalPlayerMotionSource source = slot; ILocalPlayerMotionSource source = slot;
var controller = new PlayerMovementController(new PhysicsEngine()); var controller = new PlayerMovementController(new PhysicsEngine());
@ -45,7 +45,7 @@ public sealed class LocalPlayerControllerSlotTests
[Fact] [Fact]
public void MotionPreparation_DrainsPriorAnimationBeforePublishingCandidate() public void MotionPreparation_DrainsPriorAnimationBeforePublishingCandidate()
{ {
var slot = new LocalPlayerControllerSlot(); var slot = new RuntimeLocalPlayerMovementState();
ILocalPlayerMotionSource source = slot; ILocalPlayerMotionSource source = slot;
var prior = new PlayerMovementController(new PhysicsEngine()); var prior = new PlayerMovementController(new PhysicsEngine());
var candidate = new PlayerMovementController(new PhysicsEngine()); var candidate = new PlayerMovementController(new PhysicsEngine());
@ -71,7 +71,7 @@ public sealed class LocalPlayerControllerSlotTests
[Fact] [Fact]
public void MotionPreparation_RejectsConcurrentCandidate() public void MotionPreparation_RejectsConcurrentCandidate()
{ {
var slot = new LocalPlayerControllerSlot(); var slot = new RuntimeLocalPlayerMovementState();
var first = new PlayerMovementController(new PhysicsEngine()); var first = new PlayerMovementController(new PhysicsEngine());
var second = new PlayerMovementController(new PhysicsEngine()); var second = new PlayerMovementController(new PhysicsEngine());

View file

@ -105,11 +105,10 @@ public sealed class ProjectileControllerTests
LiveEntityRecord record = fixture.Spawn(instance: 1); LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!; WorldEntity entity = record.WorldEntity!;
Assert.Null(record.AnimationRuntime); Assert.Null(record.AnimationRuntime);
var remote = new AcDream.Runtime.Physics.RemoteMotion(); var remote =
RemotePhysicsBodyInitializer.Initialize(remote.Body, record); fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
remote.Body.Position = entity.Position; remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation; remote.Body.Orientation = entity.Rotation;
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1)); Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
Assert.Same(remote.Body, record.ProjectileRuntime!.Body); Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
@ -686,7 +685,9 @@ public sealed class ProjectileControllerTests
LiveEntityRecord record = fixture.Spawn(instance: 1); LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0)); Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!; 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; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState( Assert.True(fixture.Controller.ApplyAuthoritativeState(
record, record.FinalPhysicsState, 2.0, 1, 1)); record, record.FinalPhysicsState, 2.0, 1, 1));
@ -710,7 +711,9 @@ public sealed class ProjectileControllerTests
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0)); Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!; PhysicsBody body = record.PhysicsBody!;
Vector3 position = body.Position; 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; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState( Assert.True(fixture.Controller.ApplyAuthoritativeState(
@ -949,9 +952,8 @@ public sealed class ProjectileControllerTests
var fixture = new Fixture(); var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1); LiveEntityRecord record = fixture.Spawn(instance: 1);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.Runtime.Physics.RemoteMotion(); var remote =
RemotePhysicsBodyInitializer.Initialize(remote.Body, record); fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
PhysicsStateFlags ordinaryState = PhysicsStateFlags ordinaryState =
PhysicsStateFlags.Gravity | PhysicsStateFlags.IgnoreCollisions; PhysicsStateFlags.Gravity | PhysicsStateFlags.IgnoreCollisions;
@ -1001,9 +1003,8 @@ public sealed class ProjectileControllerTests
var fixture = new Fixture(); var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1); LiveEntityRecord record = fixture.Spawn(instance: 1);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.Runtime.Physics.RemoteMotion(); var remote =
RemotePhysicsBodyInitializer.Initialize(remote.Body, record); fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
record.FinalPhysicsState = MissileState; record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState( Assert.True(fixture.Controller.ApplyAuthoritativeState(
@ -1073,15 +1074,14 @@ public sealed class ProjectileControllerTests
seedCellId: startCell, seedCellId: startCell,
isStatic: false); isStatic: false);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.Runtime.Physics.RemoteMotion(); var remote =
RemotePhysicsBodyInitializer.Initialize(remote.Body, record); fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
Assert.Equal(new Vector3(40f, 0f, 0f), remote.Body.Velocity); Assert.Equal(new Vector3(40f, 0f, 0f), remote.Body.Velocity);
Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega); Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega);
remote.Body.set_velocity(new Vector3(8f, 0f, 0f)); remote.Body.set_velocity(new Vector3(8f, 0f, 0f));
remote.Body.Omega = new Vector3(0f, 0f, 3f); remote.Body.Omega = new Vector3(0f, 0f, 3f);
remote.Body.Position = entity.Position; remote.Body.Position = entity.Position;
remote.Body.State = record.FinalPhysicsState; remote.Body.State = record.FinalPhysicsState;
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
PhysicsBody body = remote.Body; PhysicsBody body = remote.Body;
record.FinalPhysicsState = MissileState; record.FinalPhysicsState = MissileState;
@ -1139,11 +1139,10 @@ public sealed class ProjectileControllerTests
velocity: new Vector3(float.NaN, 0f, 0f)); velocity: new Vector3(float.NaN, 0f, 0f));
WorldEntity entity = record.WorldEntity!; WorldEntity entity = record.WorldEntity!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.Runtime.Physics.RemoteMotion(); var remote =
RemotePhysicsBodyInitializer.Initialize(remote.Body, record); fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
remote.Body.Position = entity.Position; remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation; remote.Body.Orientation = entity.Rotation;
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
Assert.True(float.IsNaN(record.Snapshot.Physics!.Value.Velocity!.Value.X)); Assert.True(float.IsNaN(record.Snapshot.Physics!.Value.Velocity!.Value.X));
Assert.Equal(Vector3.Zero, remote.Body.Velocity); Assert.Equal(Vector3.Zero, remote.Body.Velocity);
@ -1165,12 +1164,11 @@ public sealed class ProjectileControllerTests
LiveEntityRecord record = fixture.Spawn(instance: 1); LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!; WorldEntity entity = record.WorldEntity!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.Runtime.Physics.RemoteMotion(); var remote =
RemotePhysicsBodyInitializer.Initialize(remote.Body, record); fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
remote.Body.Position = entity.Position; remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation; remote.Body.Orientation = entity.Rotation;
remote.Body.Velocity = new Vector3(float.PositiveInfinity, 0f, 0f); remote.Body.Velocity = new Vector3(float.PositiveInfinity, 0f, 0f);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
record.FinalPhysicsState = MissileState; record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState( Assert.True(fixture.Controller.ApplyAuthoritativeState(
@ -1189,8 +1187,9 @@ public sealed class ProjectileControllerTests
LiveEntityRecord record = fixture.Spawn(instance: 1); LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0)); Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!; PhysicsBody body = record.PhysicsBody!;
var remote = new AcDream.Runtime.Physics.RemoteMotion(body); var remote =
fixture.Live.SetRemoteMotionRuntime(Guid, remote); fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
Assert.Same(body, remote.Body);
fixture.Live.RebucketLiveEntity(Guid, CellB); fixture.Live.RebucketLiveEntity(Guid, CellB);
Assert.Equal(CellB, remote.CellId); Assert.Equal(CellB, remote.CellId);

View file

@ -19,12 +19,12 @@ public sealed class RemoteTeleportControllerTests
public void Unregister(WorldEntity entity) { } public void Unregister(WorldEntity entity) { }
} }
private sealed class BodyOnlyRemote(PhysicsBody body) : ILiveEntityRemoteMotionRuntime private sealed class BodyOnlyRemote(PhysicsBody body) : IRuntimeRemoteMotion
{ {
public PhysicsBody Body { get; } = body; public PhysicsBody Body { get; } = body;
} }
private sealed class MutablePlacementRemote(PhysicsBody body) : ILiveEntityRemotePlacementRuntime private sealed class MutablePlacementRemote(PhysicsBody body) : IRuntimeRemotePlacement
{ {
private uint _cellId; private uint _cellId;
private Func<uint>? _readCell; private Func<uint>? _readCell;
@ -56,7 +56,7 @@ public sealed class RemoteTeleportControllerTests
private sealed class AlternatingBodyRemote( private sealed class AlternatingBodyRemote(
PhysicsBody first, PhysicsBody first,
PhysicsBody later) : ILiveEntityRemotePlacementRuntime PhysicsBody later) : IRuntimeRemotePlacement
{ {
private int _reads; private int _reads;
public PhysicsBody Body => _reads++ == 0 ? first : later; public PhysicsBody Body => _reads++ == 0 ? first : later;

View file

@ -190,7 +190,7 @@ public sealed class WorldRenderFrameBuilderTests
cells, cells,
mode, mode,
chase, chase,
new LocalPlayerControllerSlot(), new RuntimeLocalPlayerMovementState(),
origin); origin);
var camera = new FlyCamera { Position = new Vector3(5f, 6f, 7f) }; var camera = new FlyCamera { Position = new Vector3(5f, 6f, 7f) };
WorldCameraFrame cameraFrame = CameraFrame(camera); WorldCameraFrame cameraFrame = CameraFrame(camera);
@ -227,7 +227,7 @@ public sealed class WorldRenderFrameBuilderTests
new CellVisibility(), new CellVisibility(),
new LocalPlayerModeState(), new LocalPlayerModeState(),
new ChaseCameraInputState(), new ChaseCameraInputState(),
new LocalPlayerControllerSlot(), new RuntimeLocalPlayerMovementState(),
new LiveWorldOriginState()); new LiveWorldOriginState());
WorldCameraFrame camera = CameraFrame(new FlyCamera()); WorldCameraFrame camera = CameraFrame(new FlyCamera());

View file

@ -827,7 +827,6 @@ public sealed class CurrentGameRuntimeAdapterTests
Host, Host,
Commands, Commands,
Identity, Identity,
Entities,
EntityObjects, EntityObjects,
InventoryState, InventoryState,
Character, Character,

View file

@ -69,6 +69,13 @@ public sealed class RuntimePhysicsOwnershipTests
runtimeRoot, runtimeRoot,
"Physics", "Physics",
"RuntimePhysicsState.cs")); "RuntimePhysicsState.cs"));
string app = string.Join(
"\n",
Directory.EnumerateFiles(
appRoot,
"*.cs",
SearchOption.AllDirectories)
.Select(File.ReadAllText));
Assert.DoesNotContain( Assert.DoesNotContain(
"ResolveWithTransition(", "ResolveWithTransition(",
@ -131,6 +138,67 @@ public sealed class RuntimePhysicsOwnershipTests
"public bool TryGetPhysicsHost(", "public bool TryGetPhysicsHost(",
runtimePhysics, runtimePhysics,
StringComparison.Ordinal); 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() private static string FindRepositoryRoot()

View file

@ -411,7 +411,7 @@ public sealed class LocalPlayerTeleportControllerTests
MeshRefs = Array.Empty<MeshRef>(), MeshRefs = Array.Empty<MeshRef>(),
})!; })!;
var identity = new LocalPlayerIdentityState { ServerGuid = guid }; var identity = new LocalPlayerIdentityState { ServerGuid = guid };
var controllerSlot = new LocalPlayerControllerSlot var controllerSlot = new RuntimeLocalPlayerMovementState
{ {
Controller = new PlayerMovementController(new PhysicsEngine()), Controller = new PlayerMovementController(new PhysicsEngine()),
}; };
@ -494,7 +494,7 @@ public sealed class LocalPlayerTeleportControllerTests
MeshRefs = Array.Empty<MeshRef>(), MeshRefs = Array.Empty<MeshRef>(),
})!; })!;
var identity = new LocalPlayerIdentityState { ServerGuid = guid }; var identity = new LocalPlayerIdentityState { ServerGuid = guid };
var controllerSlot = new LocalPlayerControllerSlot var controllerSlot = new RuntimeLocalPlayerMovementState
{ {
Controller = new PlayerMovementController(new PhysicsEngine()), Controller = new PlayerMovementController(new PhysicsEngine()),
}; };

View file

@ -422,7 +422,7 @@ public sealed class StreamingFrameControllerTests
} }
internal LocalPlayerModeState Mode { get; } = new(); internal LocalPlayerModeState Mode { get; } = new();
internal LocalPlayerControllerSlot Player { get; } = new(); internal RuntimeLocalPlayerMovementState Player { get; } = new();
internal SessionSource Session { get; } = new(); internal SessionSource Session { get; } = new();
internal LiveWorldOriginState Origin { get; } = new(); internal LiveWorldOriginState Origin { get; } = new();
internal PlayerLandblockSource PlayerLandblock { get; } = new(); internal PlayerLandblockSource PlayerLandblock { get; } = new();

View file

@ -144,12 +144,7 @@ public sealed class GameWindowLiveEntityCompositionTests
typeof(LiveEntityAnimationRuntimeView<LiveEntityAnimationState>) typeof(LiveEntityAnimationRuntimeView<LiveEntityAnimationState>)
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic), .GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
field => field.Name == "_runtime"); field => field.Name == "_runtime");
FieldInfo remoteRuntime = Assert.Single(
typeof(LiveEntityRemoteMotionRuntimeView<RemoteMotion>)
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
field => field.Name == "_runtime");
Assert.Equal(typeof(ILiveEntityRuntimeSource), animationRuntime.FieldType); Assert.Equal(typeof(ILiveEntityRuntimeSource), animationRuntime.FieldType);
Assert.Equal(typeof(ILiveEntityRuntimeSource), remoteRuntime.FieldType);
FieldInfo[] projectileFields = typeof(ProjectileController).GetFields( FieldInfo[] projectileFields = typeof(ProjectileController).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic); BindingFlags.Instance | BindingFlags.NonPublic);
@ -192,7 +187,7 @@ public sealed class GameWindowLiveEntityCompositionTests
source, source,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.DoesNotContain( Assert.DoesNotContain(
"new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(() =>", "LiveEntityRemoteMotionRuntimeView",
source, source,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains("new DatProjectileSetupResolver", livePresentation); Assert.Contains("new DatProjectileSetupResolver", livePresentation);

View file

@ -371,7 +371,11 @@ public sealed class LiveEntityHydrationControllerTests
LiveEntityRecord record = fixture.Record; LiveEntityRecord record = fixture.Record;
WorldEntity entity = record.WorldEntity!; WorldEntity entity = record.WorldEntity!;
var body = new PhysicsBody(); var body = new PhysicsBody();
record.PhysicsBody = body; Assert.Same(
body,
fixture.Runtime.GetOrCreatePhysicsBody(
record.ServerGuid,
_ => body));
fixture.Relationships.OnUnparentAction = _ => fixture.Relationships.OnUnparentAction = _ =>
fixture.Runtime.WithdrawLiveEntityProjection(record) fixture.Runtime.WithdrawLiveEntityProjection(record)
? ChildUnparentDisposition.Completed ? ChildUnparentDisposition.Completed

View file

@ -874,7 +874,7 @@ public sealed class LiveEntityLifecycleStressTests
_animations.TryGetValue(id, out Animation? animation) ? animation : null; _animations.TryGetValue(id, out Animation? animation) ? animation : null;
} }
private sealed class TestRemoteMotion : ILiveEntityRemotePlacementRuntime private sealed class TestRemoteMotion : IRuntimeRemotePlacement
{ {
private uint _cellId; private uint _cellId;
private Func<uint>? _readCell; private Func<uint>? _readCell;

View file

@ -710,8 +710,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
interruptCurrentMovement); interruptCurrentMovement);
private sealed class HostConsumerMotion : private sealed class HostConsumerMotion :
ILiveEntityRemoteMotionRuntime, IRuntimeRemoteMotion,
ILiveEntityPhysicsHostConsumer IRuntimePhysicsHostConsumer
{ {
private Func<IPhysicsObjHost?>? _read; private Func<IPhysicsObjHost?>? _read;
public PhysicsBody Body { get; } = new(); public PhysicsBody Body { get; } = new();
@ -725,8 +725,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
} }
private sealed class ThrowingCellConsumerMotion : private sealed class ThrowingCellConsumerMotion :
ILiveEntityRemoteMotionRuntime, IRuntimeRemoteMotion,
ILiveEntityCanonicalRuntimeConsumer IRuntimeCanonicalPhysicsConsumer
{ {
private bool _failOnce = true; private bool _failOnce = true;
public PhysicsBody Body { get; } = new(); public PhysicsBody Body { get; } = new();
@ -748,14 +748,14 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
private sealed class AlternatingBodyMotion( private sealed class AlternatingBodyMotion(
PhysicsBody first, PhysicsBody first,
PhysicsBody second) : ILiveEntityRemoteMotionRuntime PhysicsBody second) : IRuntimeRemoteMotion
{ {
private int _reads; private int _reads;
public PhysicsBody Body => _reads++ == 0 ? first : second; public PhysicsBody Body => _reads++ == 0 ? first : second;
} }
private sealed class SequenceBodyMotion(params PhysicsBody[] bodies) : private sealed class SequenceBodyMotion(params PhysicsBody[] bodies) :
ILiveEntityRemoteMotionRuntime IRuntimeRemoteMotion
{ {
public int ReadCount { get; private set; } public int ReadCount { get; private set; }
@ -771,8 +771,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
} }
private sealed class CallbackCanonicalMotion(Action callback) : private sealed class CallbackCanonicalMotion(Action callback) :
ILiveEntityRemoteMotionRuntime, IRuntimeRemoteMotion,
ILiveEntityCanonicalRuntimeConsumer IRuntimeCanonicalPhysicsConsumer
{ {
public PhysicsBody Body { get; } = new(); public PhysicsBody Body { get; } = new();

View file

@ -48,7 +48,7 @@ public sealed class LiveEntityRuntimeTests
private sealed class EffectProfile : ILiveEntityEffectProfile { } private sealed class EffectProfile : ILiveEntityEffectProfile { }
private sealed class RemoteMotionRuntime : ILiveEntityRemoteMotionRuntime private sealed class RemoteMotionRuntime : IRuntimeRemoteMotion
{ {
public PhysicsBody Body { get; } = new(); public PhysicsBody Body { get; } = new();
} }
@ -1156,43 +1156,6 @@ public sealed class LiveEntityRuntimeTests
Assert.False(remote.Body.TransientState.HasFlag(TransientStateFlags.Active)); 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] [Fact]
public void AuthoritativeVector_WakesOrdinaryClockButPreservesStaticActivity() public void AuthoritativeVector_WakesOrdinaryClockButPreservesStaticActivity()
{ {

View file

@ -244,6 +244,10 @@ public sealed class RuntimeEntityOwnershipTests
Assert.Contains( Assert.Contains(
viewFields, viewFields,
field => field.FieldType == typeof(IRuntimeInventoryView)); field => field.FieldType == typeof(IRuntimeInventoryView));
Assert.DoesNotContain(
viewFields,
field => field.FieldType == typeof(LiveEntityRuntime)
|| field.FieldType == typeof(ClientObjectTable));
Assert.DoesNotContain( Assert.DoesNotContain(
typeof(CurrentGameRuntimeViewAdapter).GetNestedTypes( typeof(CurrentGameRuntimeViewAdapter).GetNestedTypes(
BindingFlags.NonPublic), BindingFlags.NonPublic),
@ -271,6 +275,21 @@ public sealed class RuntimeEntityOwnershipTests
Assert.DoesNotContain("_entityObjects.PublishEntity", liveSource); Assert.DoesNotContain("_entityObjects.PublishEntity", liveSource);
Assert.DoesNotContain("_directory.TryApply", liveSource); Assert.DoesNotContain("_directory.TryApply", liveSource);
Assert.DoesNotContain("_directory.RemoveActive", 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) private static bool IsPresentationType(Type type)

View file

@ -1123,7 +1123,10 @@ public sealed class RuntimeEntityObjectLifetimeTests
lifetime.Dispose(); lifetime.Dispose();
Assert.Equal( Assert.Equal(
default(RuntimeEntityObjectOwnershipSnapshot), default(RuntimeEntityObjectOwnershipSnapshot) with
{
IsDisposed = true,
},
lifetime.CaptureOwnership()); lifetime.CaptureOwnership());
} }
@ -1158,7 +1161,10 @@ public sealed class RuntimeEntityObjectLifetimeTests
lifetime.Dispose(); lifetime.Dispose();
Assert.Equal( Assert.Equal(
default(RuntimeEntityObjectOwnershipSnapshot), default(RuntimeEntityObjectOwnershipSnapshot) with
{
IsDisposed = true,
},
lifetime.CaptureOwnership()); lifetime.CaptureOwnership());
} }

View file

@ -3,11 +3,60 @@ using AcDream.Core.Items;
using AcDream.Core.Social; using AcDream.Core.Social;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay; using AcDream.Runtime.Gameplay;
using AcDream.Runtime;
namespace AcDream.Runtime.Tests.Gameplay; namespace AcDream.Runtime.Tests.Gameplay;
public sealed class RuntimeGameplayOwnershipTests 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] [Fact]
public void CombinedLedgerConvergesEveryGameplayOwnerAndSubscription() public void CombinedLedgerConvergesEveryGameplayOwnerAndSubscription()
{ {

View file

@ -340,6 +340,49 @@ public sealed class RuntimePhysicsStateTests
Assert.Equal(0, first.Physics.Engine.LandblockCount); 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] [Fact]
public void RemoteBindingOwnsCanonicalBodyCellAndTypedCellPublication() public void RemoteBindingOwnsCanonicalBodyCellAndTypedCellPublication()
{ {
@ -368,6 +411,66 @@ public sealed class RuntimePhysicsStateTests
Assert.Equal(0, lifetime.Physics.SpatialRemoteCount); 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] [Fact]
public void RemotePhysicsTickUsesCanonicalRuntimeAndPublishesPoseSnapshot() public void RemotePhysicsTickUsesCanonicalRuntimeAndPublishesPoseSnapshot()
{ {
@ -523,7 +626,11 @@ public sealed class RuntimePhysicsStateTests
0u); 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( var position = new CreateObject.ServerPosition(
0x0101FFFFu, 0x0101FFFFu,
@ -559,9 +666,9 @@ public sealed class RuntimePhysicsStateTests
Friction: null, Friction: null,
Elasticity: null, Elasticity: null,
Translucency: null, Translucency: null,
Velocity: null, Velocity: velocity,
Acceleration: null, Acceleration: null,
AngularVelocity: null, AngularVelocity: angularVelocity,
DefaultScriptType: null, DefaultScriptType: null,
DefaultScriptIntensity: null, DefaultScriptIntensity: null,
Timestamps: timestamps); Timestamps: timestamps);

View file

@ -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<InvalidOperationException>(() =>
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<string> 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<ClientObject> 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<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"runtime projectile",
null,
null,
null,
PhysicsState: physics.RawState,
InstanceSequence: instance,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
}
}