From 5882b308c146051c46465bc6190cee8c4c55852e Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jul 2026 13:17:58 +0200 Subject: [PATCH] refactor(physics): extract remote motion runtime --- .../Physics/ProjectileController.cs | 2 +- src/AcDream.App/Physics/RemoteMotion.cs | 307 ++++++++++++++++++ .../Physics/RemotePhysicsUpdater.cs | 2 +- src/AcDream.App/Rendering/GameWindow.cs | 302 +---------------- .../Rendering/LiveEntityAnimationScheduler.cs | 2 +- .../LiveEntityOrdinaryPhysicsUpdaterTests.cs | 4 +- .../Physics/ProjectileControllerTests.cs | 20 +- .../Physics/RemotePhysicsUpdaterTests.cs | 36 +- .../Physics/RemoteTeleportControllerTests.cs | 22 +- .../Physics/RemoteTeleportPlacementTests.cs | 10 +- .../LiveEntityAnimationSchedulerTests.cs | 10 +- .../Motion/RemoteChaseEndToEndHarnessTests.cs | 12 +- 12 files changed, 368 insertions(+), 361 deletions(-) create mode 100644 src/AcDream.App/Physics/RemoteMotion.cs diff --git a/src/AcDream.App/Physics/ProjectileController.cs b/src/AcDream.App/Physics/ProjectileController.cs index 7e68faef..7e4ad350 100644 --- a/src/AcDream.App/Physics/ProjectileController.cs +++ b/src/AcDream.App/Physics/ProjectileController.cs @@ -5,7 +5,7 @@ using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; using DatReaderWriter.DBObjs; -using RemoteMotion = AcDream.App.Rendering.GameWindow.RemoteMotion; +using RemoteMotion = AcDream.App.Physics.RemoteMotion; namespace AcDream.App.Physics; diff --git a/src/AcDream.App/Physics/RemoteMotion.cs b/src/AcDream.App/Physics/RemoteMotion.cs new file mode 100644 index 00000000..7b35d0bd --- /dev/null +++ b/src/AcDream.App/Physics/RemoteMotion.cs @@ -0,0 +1,307 @@ +using AcDream.App.Rendering; +using AcDream.App.World; + +namespace AcDream.App.Physics; + +/// +/// Per-remote-entity physics + motion stack — verbatim application of +/// retail's client-side motion pipeline to every remote. Mirrors retail +/// CPhysicsObj::update_object (0x00515D10) → +/// CPhysicsObj::UpdateObjectInternal (0x005156B0) → +/// CPhysicsObj::UpdatePositionInternal (0x00512C30) → +/// CPhysicsObj::UpdatePhysicsInternal (0x00510700), and ACE's +/// PhysicsObj.cs port. +/// +/// +/// Retail has NO special "interpolator" for remote entities — it runs +/// the full motion state machine on every entity, local or remote, +/// and reconciles via hard-snap on UpdatePosition. This class simply +/// pairs a with its +/// so each +/// remote gets the same treatment as the local player. +/// +/// +internal sealed class RemoteMotion : + AcDream.App.World.ILiveEntityRemotePlacementRuntime +{ + public AcDream.Core.Physics.PhysicsBody Body { get; } + AcDream.Core.Physics.PhysicsBody AcDream.App.World.ILiveEntityRemoteMotionRuntime.Body => Body; + + /// R5-V5: retail CPhysicsObj::movement_manager — the + /// ONE per-entity owner of the interp + moveto pair (acclient.h + /// /* 3463 */). Constructed here with the interp; the + /// MoveToManager side arrives via MoveToFactory + + /// MakeMoveToManager() in EnsureRemoteMotionBindings. + /// / below are views of its + /// children (retail get_minterp-style access), kept so the + /// dozens of existing call sites read unchanged. + public AcDream.Core.Physics.Motion.MovementManager Movement; + + public AcDream.Core.Physics.MotionInterpreter Motion => Movement.Minterp; + /// R3-W4: the persistent per-remote dispatch sink (created + /// once by EnsureRemoteMotionBindings; also bound as + /// Motion.DefaultSink so LeaveGround/HitGround re-applies dispatch + /// cycles through the motion-table backend). + public AcDream.Core.Physics.Motion.MotionTableDispatchSink? Sink; + /// Last UpdatePosition timestamp — drives body.update_object sub-stepping. + public double LastServerPosTime; + /// Last known server position — kept for diagnostics / HUD. + public System.Numerics.Vector3 LastServerPos; + /// + /// #184: the body position at which this remote's collision SHADOW was + /// last (re-)registered. The per-tick shadow-follows-resolved sync + /// (SyncRemoteShadowToBody) skips re-flooding when the body has + /// not moved > ε since this — a resting/idle-town crowd doesn't churn + /// the registry, while a moving/de-overlapping crowd re-registers every + /// tick. Sentinel Zero forces the first sync. + /// + public System.Numerics.Vector3 LastShadowSyncPos; + /// + /// Complete root orientation paired with . + /// Multipart and off-centre Setup collision geometry must be re-flooded + /// when the object turns in place, not only when its origin translates. + /// The default zero quaternion is the first-sync sentinel. + /// + public System.Numerics.Quaternion LastShadowSyncOrientation; + /// + /// Latest server-authoritative velocity for NPC/monster smoothing. + /// Prefer the HasVelocity vector from UpdatePosition; when ACE omits + /// it for a server-controlled creature, derive it from consecutive + /// authoritative positions instead of guessing from player RUM state. + /// + public System.Numerics.Vector3 ServerVelocity; + public bool HasServerVelocity; + + /// R4-V4: the entity's verbatim retail MoveToManager + /// (server-directed movement), constructed + seam-bound by + /// EnsureRemoteMotionBindings beside the R2-Q5/R3 binds. R5-V5: + /// owned by ; this is the child view. + public AcDream.Core.Physics.Motion.MoveToManager? MoveTo => Movement.MoveTo; + + // R5-V2: the entity's CPhysicsObj stand-in — owns the TargetManager + // voyeur system (retail CPhysicsObj::target_manager). Replaces the + // AP-79 TrackedTarget* poll fields: the MoveToManager's set_target/ + // clear_target/quantum seams route here, HandleTargetting ticks per + // frame, and OTHER entities resolve this one via GameWindow._physicsHosts. + public EntityPhysicsHost? Host; + /// + /// True while a server MoveToObject/MoveToPosition packet is the + /// active locomotion source. Retail runs these through MoveToManager + /// and CMotionInterp; the per-tick remote driver consults this to + /// decide whether to feed body steering through + /// instead of + /// the InterpretedMotionState path. + /// + public bool ServerMoveToActive; + + /// + /// True once a MoveTo packet's full path payload (Origin + thresholds) + /// has been parsed and the world-converted destination is stored on + /// . Cleared on arrival or when + /// the next non-MoveTo UpdateMotion replaces the locomotion source. + /// Phase L.1c (2026-04-28). + /// + public bool HasMoveToDestination; + + /// + /// World-space destination from the most recent MoveTo packet's + /// Origin field, converted via the same landblock-grid + /// arithmetic OnLivePositionUpdated uses. + /// + public System.Numerics.Vector3 MoveToDestinationWorld; + + /// + /// min_distance from the MoveTo packet's MovementParameters. + /// Used by as + /// the chase-arrival threshold per retail + /// MoveToManager::HandleMoveToPosition. + /// + public float MoveToMinDistance; + + /// + /// distance_to_object from the MoveTo packet. Reserved for + /// the flee branch (move_away); chase uses + /// . + /// + public float MoveToDistanceToObject; + + /// + /// True if MovementParameters bit 9 (move_towards, mask + /// 0x200) is set on the active packet — i.e. this is a + /// chase. False = flee (move_away) or static target. + /// + public bool MoveToMoveTowards; + + /// + /// Seconds-since-epoch timestamp of the most recent MoveTo packet + /// for this entity. Used by the per-tick driver to give up + /// steering when no refresh has arrived for + /// + /// — typically because the entity left our streaming view and + /// the server stopped broadcasting its MoveTo updates. + /// + public double LastMoveToPacketTime; + /// + /// Full 32-bit cell ID from the latest UpdatePosition. High 16 bits + /// = landblock (LBx,LBy), low 16 bits = cell index (outdoor 0x0001- + /// 0x0040, indoor 0x0100+). Fed into + /// + /// so the retail sphere-sweep can look up the right terrain/EnvCell + /// polygons for each remote's per-frame motion. Zero until the first + /// UP lands, which disables the transition step for that frame + /// (Euler alone, matching pre-wire behavior). + /// + private uint _cellId; + private Func? _canonicalCellReader; + private Action? _canonicalCellWriter; + + /// + /// Canonical full cell for this one live CPhysicsObj. Ordinary remotes + /// retain the local backing value. A MovementManager attached to an + /// existing projectile delegates reads and writes to + /// , so projectile prediction, + /// Movement packets, and spatial rebucketing cannot develop competing + /// cell identities. + /// + public uint CellId + { + get => _canonicalCellReader?.Invoke() ?? _cellId; + set + { + _cellId = value; + _canonicalCellWriter?.Invoke(value); + } + } + + System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPosition + { + get => LastServerPos; + set => LastServerPos = value; + } + + double AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPositionTime + { + get => LastServerPosTime; + set => LastServerPosTime = value; + } + + System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastShadowSyncPosition + { + get => LastShadowSyncPos; + set => LastShadowSyncPos = value; + } + + System.Numerics.Quaternion AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastShadowSyncOrientation + { + get => LastShadowSyncOrientation; + set => LastShadowSyncOrientation = value; + } + + bool AcDream.App.World.ILiveEntityRemotePlacementRuntime.Airborne + { + get => Airborne; + set => Airborne = value; + } + + void AcDream.App.World.ILiveEntityRemotePlacementRuntime.HitGround() => + Movement.HitGround(); + + void AcDream.App.World.ILiveEntityRemotePlacementRuntime.LeaveGround() => + Motion.LeaveGround(); + + /// + /// K-fix9 (2026-04-26): true while the remote is airborne (jump + /// arc in flight). Set when a 0xF74E VectorUpdate arrives with + /// non-trivial +Z velocity; cleared when the next UpdatePosition + /// snaps to a new ground location. While true, the per-tick + /// remote update SKIPS the "force OnWalkable + apply_current_movement" + /// step that would otherwise stomp the body's Z velocity each + /// frame, AND enables gravity so the parabolic arc actually plays + /// out between server snaps. + /// + public bool Airborne; + + /// + /// Per-remote position-waypoint queue + catch-up math (retail's + /// CPhysicsObj::InterpolateTo + InterpolationManager::adjust_offset). + /// Drives per-tick body translation for grounded player remotes + /// via . + /// + public AcDream.Core.Physics.InterpolationManager Interp { get; } = + new AcDream.Core.Physics.InterpolationManager(); + + /// + /// Per-frame combiner for animation root motion + InterpolationManager + /// correction. Mirrors retail UpdatePositionInternal @ 0x00512c30: + /// queue catch-up REPLACES anim when active; anim stands when queue + /// is idle. + /// + public AcDream.Core.Physics.RemoteMotionCombiner Position { get; } = + new AcDream.Core.Physics.RemoteMotionCombiner(); + + /// Reusable complete delta frame shared by interpolation, + /// Sticky/Constraint, and the final body composition. + public AcDream.Core.Physics.Motion.MotionDeltaFrame PositionManagerDeltaScratch { get; } = + new AcDream.Core.Physics.Motion.MotionDeltaFrame(); + + /// + /// Diagnostic-only (gated on ACDREAM_REMOTE_VEL_DIAG=1): the + /// previous UpdatePosition's world position + timestamp. The per-tick + /// path compares the server's broadcast pace with the literal root + /// displacement emitted by CSequence::update. The old inferred + /// walk/run velocity was the source of the periodic overshoot this + /// diagnostic was introduced to find. + /// + public System.Numerics.Vector3 PrevServerPos; + public double PrevServerPosTime; + public double LastOmegaDiagLogTime; + + /// + /// Diagnostic-only: maximum scaled CSequence root-motion speed + /// observed since the last UpdatePosition arrival. The next UP + /// compares it with the server's observed pace. Reset on each UP. + /// + public float MaxRootMotionSpeedSinceLastUP; + + public RemoteMotion(AcDream.Core.Physics.PhysicsBody? sharedBody = null) + { + Body = sharedBody ?? new AcDream.Core.Physics.PhysicsBody + { + // Remotes don't simulate gravity — server owns Z. Force + // Contact + OnWalkable + Active so apply_current_movement + // writes velocity through every tick (the gate in + // MotionInterpreter.apply_current_movement is + // PhysicsObj.OnWalkable). + State = AcDream.Core.Physics.PhysicsStateFlags.ReportCollisions, + TransientState = AcDream.Core.Physics.TransientStateFlags.Contact + | AcDream.Core.Physics.TransientStateFlags.OnWalkable + | AcDream.Core.Physics.TransientStateFlags.Active, + // R4-V5 door-swing fix: a RemoteMotion exists only for a + // WORLD entity — retail's physics_obj->cell is non-null the + // moment the object is placed. Without this, the interp's + // detached-object guard stripped every dispatched + // transition link (see PhysicsBody.InWorld). + InWorld = true, + }; + // R5-V5: the interp is owned by the MovementManager facade + // (retail CPhysicsObj::movement_manager -> motion_interpreter). + // acdream constructs it eagerly here — retail's lazy-create + // window is never observable (see MovementManager's class doc). + Movement = new AcDream.Core.Physics.Motion.MovementManager( + new AcDream.Core.Physics.MotionInterpreter(Body) + { + // R4-V5 #160 fix: retail remotes carry a weenie whose + // InqRunRate FAILS, landing apply_run_to_command on + // my_run_rate (the M13 wire feed). A null weenie took the + // degenerate 1.0 branch — observer-side run movetos played + // and moved in slow motion. See RemoteWeenie. + WeenieObj = new AcDream.Core.Physics.RemoteWeenie(), + }); + } + + public void BindCanonicalCell(Func read, Action write) + { + _canonicalCellReader = read ?? throw new ArgumentNullException(nameof(read)); + _canonicalCellWriter = write ?? throw new ArgumentNullException(nameof(write)); + } +} diff --git a/src/AcDream.App/Physics/RemotePhysicsUpdater.cs b/src/AcDream.App/Physics/RemotePhysicsUpdater.cs index 4c6b039b..412ef6e2 100644 --- a/src/AcDream.App/Physics/RemotePhysicsUpdater.cs +++ b/src/AcDream.App/Physics/RemotePhysicsUpdater.cs @@ -1,4 +1,4 @@ -using RemoteMotion = AcDream.App.Rendering.GameWindow.RemoteMotion; +using RemoteMotion = AcDream.App.Physics.RemoteMotion; using LiveEntityAnimationState = AcDream.App.Rendering.LiveEntityAnimationState; namespace AcDream.App.Physics; diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index fb194182..dcd6ba55 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1,4 +1,5 @@ using AcDream.Core.Plugins; +using AcDream.App.Physics; using AcDream.App.World; using AcDream.Content; using DatReaderWriter; @@ -421,307 +422,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext private AcDream.App.World.LiveEntityRuntime? _liveEntities; private AcDream.App.World.LiveEntityLivenessController? _liveEntityLiveness; - /// - /// Per-remote-entity physics + motion stack — verbatim application of - /// retail's client-side motion pipeline to every remote. Mirrors - /// retail FUN_00515020 update_objectFUN_00513730 - /// UpdatePositionInternalFUN_005111D0 - /// UpdatePhysicsInternal, and ACE's PhysicsObj.cs port. - /// - /// - /// Retail has NO special "interpolator" for remote entities — it runs - /// the full motion state machine on every entity, local or remote, - /// and reconciles via hard-snap on UpdatePosition. This class simply - /// pairs a with its - /// so each - /// remote gets the same treatment as the local player. - /// - /// - internal sealed class RemoteMotion : - AcDream.App.World.ILiveEntityRemotePlacementRuntime - { - public AcDream.Core.Physics.PhysicsBody Body { get; } - AcDream.Core.Physics.PhysicsBody AcDream.App.World.ILiveEntityRemoteMotionRuntime.Body => Body; - - /// R5-V5: retail CPhysicsObj::movement_manager — the - /// ONE per-entity owner of the interp + moveto pair (acclient.h - /// /* 3463 */). Constructed here with the interp; the - /// MoveToManager side arrives via MoveToFactory + - /// MakeMoveToManager() in EnsureRemoteMotionBindings. - /// / below are views of its - /// children (retail get_minterp-style access), kept so the - /// dozens of existing call sites read unchanged. - public AcDream.Core.Physics.Motion.MovementManager Movement; - - public AcDream.Core.Physics.MotionInterpreter Motion => Movement.Minterp; - /// R3-W4: the persistent per-remote dispatch sink (created - /// once by EnsureRemoteMotionBindings; also bound as - /// Motion.DefaultSink so LeaveGround/HitGround re-applies dispatch - /// cycles through the motion-table backend). - public AcDream.Core.Physics.Motion.MotionTableDispatchSink? Sink; - /// Last UpdatePosition timestamp — drives body.update_object sub-stepping. - public double LastServerPosTime; - /// Last known server position — kept for diagnostics / HUD. - public System.Numerics.Vector3 LastServerPos; - /// - /// #184: the body position at which this remote's collision SHADOW was - /// last (re-)registered. The per-tick shadow-follows-resolved sync - /// (SyncRemoteShadowToBody) skips re-flooding when the body has - /// not moved > ε since this — a resting/idle-town crowd doesn't churn - /// the registry, while a moving/de-overlapping crowd re-registers every - /// tick. Sentinel Zero forces the first sync. - /// - public System.Numerics.Vector3 LastShadowSyncPos; - /// - /// Complete root orientation paired with . - /// Multipart and off-centre Setup collision geometry must be re-flooded - /// when the object turns in place, not only when its origin translates. - /// The default zero quaternion is the first-sync sentinel. - /// - public System.Numerics.Quaternion LastShadowSyncOrientation; - /// - /// Latest server-authoritative velocity for NPC/monster smoothing. - /// Prefer the HasVelocity vector from UpdatePosition; when ACE omits - /// it for a server-controlled creature, derive it from consecutive - /// authoritative positions instead of guessing from player RUM state. - /// - public System.Numerics.Vector3 ServerVelocity; - public bool HasServerVelocity; - - /// R4-V4: the entity's verbatim retail MoveToManager - /// (server-directed movement), constructed + seam-bound by - /// EnsureRemoteMotionBindings beside the R2-Q5/R3 binds. R5-V5: - /// owned by ; this is the child view. - public AcDream.Core.Physics.Motion.MoveToManager? MoveTo => Movement.MoveTo; - - // R5-V2: the entity's CPhysicsObj stand-in — owns the TargetManager - // voyeur system (retail CPhysicsObj::target_manager). Replaces the - // AP-79 TrackedTarget* poll fields: the MoveToManager's set_target/ - // clear_target/quantum seams route here, HandleTargetting ticks per - // frame, and OTHER entities resolve this one via GameWindow._physicsHosts. - public EntityPhysicsHost? Host; - /// - /// True while a server MoveToObject/MoveToPosition packet is the - /// active locomotion source. Retail runs these through MoveToManager - /// and CMotionInterp; the per-tick remote driver consults this to - /// decide whether to feed body steering through - /// instead of - /// the InterpretedMotionState path. - /// - public bool ServerMoveToActive; - - /// - /// True once a MoveTo packet's full path payload (Origin + thresholds) - /// has been parsed and the world-converted destination is stored on - /// . Cleared on arrival or when - /// the next non-MoveTo UpdateMotion replaces the locomotion source. - /// Phase L.1c (2026-04-28). - /// - public bool HasMoveToDestination; - - /// - /// World-space destination from the most recent MoveTo packet's - /// Origin field, converted via the same landblock-grid - /// arithmetic OnLivePositionUpdated uses. - /// - public System.Numerics.Vector3 MoveToDestinationWorld; - - /// - /// min_distance from the MoveTo packet's MovementParameters. - /// Used by as - /// the chase-arrival threshold per retail - /// MoveToManager::HandleMoveToPosition. - /// - public float MoveToMinDistance; - - /// - /// distance_to_object from the MoveTo packet. Reserved for - /// the flee branch (move_away); chase uses - /// . - /// - public float MoveToDistanceToObject; - - /// - /// True if MovementParameters bit 9 (move_towards, mask - /// 0x200) is set on the active packet — i.e. this is a - /// chase. False = flee (move_away) or static target. - /// - public bool MoveToMoveTowards; - - /// - /// Seconds-since-epoch timestamp of the most recent MoveTo packet - /// for this entity. Used by the per-tick driver to give up - /// steering when no refresh has arrived for - /// - /// — typically because the entity left our streaming view and - /// the server stopped broadcasting its MoveTo updates. - /// - public double LastMoveToPacketTime; - /// - /// Full 32-bit cell ID from the latest UpdatePosition. High 16 bits - /// = landblock (LBx,LBy), low 16 bits = cell index (outdoor 0x0001- - /// 0x0040, indoor 0x0100+). Fed into - /// - /// so the retail sphere-sweep can look up the right terrain/EnvCell - /// polygons for each remote's per-frame motion. Zero until the first - /// UP lands, which disables the transition step for that frame - /// (Euler alone, matching pre-wire behavior). - /// - private uint _cellId; - private Func? _canonicalCellReader; - private Action? _canonicalCellWriter; - - /// - /// Canonical full cell for this one live CPhysicsObj. Ordinary remotes - /// retain the local backing value. A MovementManager attached to an - /// existing projectile delegates reads and writes to - /// , so projectile prediction, - /// Movement packets, and spatial rebucketing cannot develop competing - /// cell identities. - /// - public uint CellId - { - get => _canonicalCellReader?.Invoke() ?? _cellId; - set - { - _cellId = value; - _canonicalCellWriter?.Invoke(value); - } - } - - System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPosition - { - get => LastServerPos; - set => LastServerPos = value; - } - - double AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPositionTime - { - get => LastServerPosTime; - set => LastServerPosTime = value; - } - - System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastShadowSyncPosition - { - get => LastShadowSyncPos; - set => LastShadowSyncPos = value; - } - - System.Numerics.Quaternion AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastShadowSyncOrientation - { - get => LastShadowSyncOrientation; - set => LastShadowSyncOrientation = value; - } - - bool AcDream.App.World.ILiveEntityRemotePlacementRuntime.Airborne - { - get => Airborne; - set => Airborne = value; - } - - void AcDream.App.World.ILiveEntityRemotePlacementRuntime.HitGround() => - Movement.HitGround(); - - void AcDream.App.World.ILiveEntityRemotePlacementRuntime.LeaveGround() => - Motion.LeaveGround(); - - /// - /// K-fix9 (2026-04-26): true while the remote is airborne (jump - /// arc in flight). Set when a 0xF74E VectorUpdate arrives with - /// non-trivial +Z velocity; cleared when the next UpdatePosition - /// snaps to a new ground location. While true, the per-tick - /// remote update SKIPS the "force OnWalkable + apply_current_movement" - /// step that would otherwise stomp the body's Z velocity each - /// frame, AND enables gravity so the parabolic arc actually plays - /// out between server snaps. - /// - public bool Airborne; - - /// - /// Per-remote position-waypoint queue + catch-up math (retail's - /// CPhysicsObj::InterpolateTo + InterpolationManager::adjust_offset). - /// Drives per-tick body translation for grounded player remotes - /// via . - /// - public AcDream.Core.Physics.InterpolationManager Interp { get; } = - new AcDream.Core.Physics.InterpolationManager(); - - /// - /// Per-frame combiner for animation root motion + InterpolationManager - /// correction. Mirrors retail UpdatePositionInternal @ 0x00512c30: - /// queue catch-up REPLACES anim when active; anim stands when queue - /// is idle. - /// - public AcDream.Core.Physics.RemoteMotionCombiner Position { get; } = - new AcDream.Core.Physics.RemoteMotionCombiner(); - - /// Reusable complete delta frame shared by interpolation, - /// Sticky/Constraint, and the final body composition. - public AcDream.Core.Physics.Motion.MotionDeltaFrame PositionManagerDeltaScratch { get; } = - new AcDream.Core.Physics.Motion.MotionDeltaFrame(); - - /// - /// Diagnostic-only (gated on ACDREAM_REMOTE_VEL_DIAG=1): the - /// previous UpdatePosition's world position + timestamp. The per-tick - /// path compares the server's broadcast pace with the literal root - /// displacement emitted by CSequence::update. The old inferred - /// walk/run velocity was the source of the periodic overshoot this - /// diagnostic was introduced to find. - /// - public System.Numerics.Vector3 PrevServerPos; - public double PrevServerPosTime; - public double LastOmegaDiagLogTime; - - /// - /// Diagnostic-only: maximum scaled CSequence root-motion speed - /// observed since the last UpdatePosition arrival. The next UP - /// compares it with the server's observed pace. Reset on each UP. - /// - public float MaxRootMotionSpeedSinceLastUP; - - public RemoteMotion(AcDream.Core.Physics.PhysicsBody? sharedBody = null) - { - Body = sharedBody ?? new AcDream.Core.Physics.PhysicsBody - { - // Remotes don't simulate gravity — server owns Z. Force - // Contact + OnWalkable + Active so apply_current_movement - // writes velocity through every tick (the gate in - // MotionInterpreter.apply_current_movement is - // PhysicsObj.OnWalkable). - State = AcDream.Core.Physics.PhysicsStateFlags.ReportCollisions, - TransientState = AcDream.Core.Physics.TransientStateFlags.Contact - | AcDream.Core.Physics.TransientStateFlags.OnWalkable - | AcDream.Core.Physics.TransientStateFlags.Active, - // R4-V5 door-swing fix: a RemoteMotion exists only for a - // WORLD entity — retail's physics_obj->cell is non-null the - // moment the object is placed. Without this, the interp's - // detached-object guard stripped every dispatched - // transition link (see PhysicsBody.InWorld). - InWorld = true, - }; - // R5-V5: the interp is owned by the MovementManager facade - // (retail CPhysicsObj::movement_manager -> motion_interpreter). - // acdream constructs it eagerly here — retail's lazy-create - // window is never observable (see MovementManager's class doc). - Movement = new AcDream.Core.Physics.Motion.MovementManager( - new AcDream.Core.Physics.MotionInterpreter(Body) - { - // R4-V5 #160 fix: retail remotes carry a weenie whose - // InqRunRate FAILS, landing apply_run_to_command on - // my_run_rate (the M13 wire feed). A null weenie took the - // degenerate 1.0 branch — observer-side run movetos played - // and moved in slow motion. See RemoteWeenie. - WeenieObj = new AcDream.Core.Physics.RemoteWeenie(), - }); - } - - public void BindCanonicalCell(Func read, Action write) - { - _canonicalCellReader = read ?? throw new ArgumentNullException(nameof(read)); - _canonicalCellWriter = write ?? throw new ArgumentNullException(nameof(write)); - } - } - /// Soft-snap decay rate (1/sec). At this rate the residual /// halves every 1/rate seconds. 8.0 → ~100ms half-life, so even a /// 2m residual fades within ~300ms without visible snap. diff --git a/src/AcDream.App/Rendering/LiveEntityAnimationScheduler.cs b/src/AcDream.App/Rendering/LiveEntityAnimationScheduler.cs index 5e156244..b7b275cf 100644 --- a/src/AcDream.App/Rendering/LiveEntityAnimationScheduler.cs +++ b/src/AcDream.App/Rendering/LiveEntityAnimationScheduler.cs @@ -5,7 +5,7 @@ using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using AcDream.Core.World; using DatReaderWriter.Types; -using RemoteMotion = AcDream.App.Rendering.GameWindow.RemoteMotion; +using RemoteMotion = AcDream.App.Physics.RemoteMotion; namespace AcDream.App.Rendering; diff --git a/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs b/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs index 5d50faa0..040c23c9 100644 --- a/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs +++ b/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs @@ -41,7 +41,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests ParentCellId = SourceCell, })!; - var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion + var remote = new AcDream.App.Physics.RemoteMotion { CellId = SourceCell, }; @@ -108,7 +108,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests MeshRefs = Array.Empty(), ParentCellId = SourceCell, })!; - var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion + var remote = new AcDream.App.Physics.RemoteMotion { CellId = SourceCell, }; diff --git a/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs b/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs index aefa2284..2bc55d79 100644 --- a/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs +++ b/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs @@ -104,7 +104,7 @@ public sealed class ProjectileControllerTests LiveEntityRecord record = fixture.Spawn(instance: 1); WorldEntity entity = record.WorldEntity!; Assert.Null(record.AnimationRuntime); - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); RemotePhysicsBodyInitializer.Initialize(remote.Body, record); remote.Body.Position = entity.Position; remote.Body.Orientation = entity.Rotation; @@ -683,7 +683,7 @@ public sealed class ProjectileControllerTests LiveEntityRecord record = fixture.Spawn(instance: 1); Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0)); PhysicsBody body = record.PhysicsBody!; - fixture.Live.SetRemoteMotionRuntime(Guid, new GameWindow.RemoteMotion(body)); + fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.App.Physics.RemoteMotion(body)); record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; Assert.True(fixture.Controller.ApplyAuthoritativeState( record, record.FinalPhysicsState, 2.0, 1, 1)); @@ -707,7 +707,7 @@ public sealed class ProjectileControllerTests Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0)); PhysicsBody body = record.PhysicsBody!; Vector3 position = body.Position; - fixture.Live.SetRemoteMotionRuntime(Guid, new GameWindow.RemoteMotion(body)); + fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.App.Physics.RemoteMotion(body)); record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; Assert.True(fixture.Controller.ApplyAuthoritativeState( @@ -946,7 +946,7 @@ public sealed class ProjectileControllerTests var fixture = new Fixture(); LiveEntityRecord record = fixture.Spawn(instance: 1); record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); RemotePhysicsBodyInitializer.Initialize(remote.Body, record); fixture.Live.SetRemoteMotionRuntime(Guid, remote); @@ -998,7 +998,7 @@ public sealed class ProjectileControllerTests var fixture = new Fixture(); LiveEntityRecord record = fixture.Spawn(instance: 1); record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); RemotePhysicsBodyInitializer.Initialize(remote.Body, record); fixture.Live.SetRemoteMotionRuntime(Guid, remote); @@ -1070,7 +1070,7 @@ public sealed class ProjectileControllerTests seedCellId: startCell, isStatic: false); record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); RemotePhysicsBodyInitializer.Initialize(remote.Body, record); Assert.Equal(new Vector3(40f, 0f, 0f), remote.Body.Velocity); Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega); @@ -1136,7 +1136,7 @@ public sealed class ProjectileControllerTests velocity: new Vector3(float.NaN, 0f, 0f)); WorldEntity entity = record.WorldEntity!; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); RemotePhysicsBodyInitializer.Initialize(remote.Body, record); remote.Body.Position = entity.Position; remote.Body.Orientation = entity.Rotation; @@ -1162,7 +1162,7 @@ public sealed class ProjectileControllerTests LiveEntityRecord record = fixture.Spawn(instance: 1); WorldEntity entity = record.WorldEntity!; record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions; - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); RemotePhysicsBodyInitializer.Initialize(remote.Body, record); remote.Body.Position = entity.Position; remote.Body.Orientation = entity.Rotation; @@ -1186,7 +1186,7 @@ public sealed class ProjectileControllerTests LiveEntityRecord record = fixture.Spawn(instance: 1); Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0)); PhysicsBody body = record.PhysicsBody!; - var remote = new GameWindow.RemoteMotion(body); + var remote = new AcDream.App.Physics.RemoteMotion(body); fixture.Live.SetRemoteMotionRuntime(Guid, remote); fixture.Live.RebucketLiveEntity(Guid, CellB); @@ -1198,7 +1198,7 @@ public sealed class ProjectileControllerTests Assert.Same(body, record.RemoteMotionRuntime!.Body); Assert.Same(body, record.ProjectileRuntime!.Body); - var replacement = new GameWindow.RemoteMotion(body); + var replacement = new AcDream.App.Physics.RemoteMotion(body); fixture.Live.SetRemoteMotionRuntime(Guid, replacement); remote.CellId = CellB; Assert.Equal(CellA, record.FullCellId); diff --git a/tests/AcDream.App.Tests/Physics/RemotePhysicsUpdaterTests.cs b/tests/AcDream.App.Tests/Physics/RemotePhysicsUpdaterTests.cs index afc96a3d..02b2e6a1 100644 --- a/tests/AcDream.App.Tests/Physics/RemotePhysicsUpdaterTests.cs +++ b/tests/AcDream.App.Tests/Physics/RemotePhysicsUpdaterTests.cs @@ -68,7 +68,7 @@ public sealed class RemotePhysicsUpdaterTests MeshRefs = Array.Empty(), ParentCellId = cellId, }; - var motion = new GameWindow.RemoteMotion + var motion = new AcDream.App.Physics.RemoteMotion { LastShadowSyncPos = Vector3.Zero, LastShadowSyncOrientation = Quaternion.Identity, @@ -151,7 +151,7 @@ public sealed class RemotePhysicsUpdaterTests PartTemplate = Array.Empty(), PartAvailability = Array.Empty(), }; - var motion = new GameWindow.RemoteMotion(); + var motion = new AcDream.App.Physics.RemoteMotion(); motion.Body.Position = entity.Position; motion.Body.Orientation = entity.Rotation; motion.Body.TransientState = TransientStateFlags.Contact @@ -217,7 +217,7 @@ public sealed class RemotePhysicsUpdaterTests PartTemplate = Array.Empty(), PartAvailability = Array.Empty(), }; - var motion = new GameWindow.RemoteMotion + var motion = new AcDream.App.Physics.RemoteMotion { Airborne = true, }; @@ -272,7 +272,7 @@ public sealed class RemotePhysicsUpdaterTests PartTemplate = Array.Empty(), PartAvailability = Array.Empty(), }; - var motion = new GameWindow.RemoteMotion(); + var motion = new AcDream.App.Physics.RemoteMotion(); motion.Body.Position = entity.Position; motion.Body.Orientation = initial; motion.Interp.Enqueue( @@ -308,7 +308,7 @@ public sealed class RemotePhysicsUpdaterTests [Fact] public void TickHidden_AppliesPositionManagerOffsetWithoutAdvancingPhysics() { - var motion = new GameWindow.RemoteMotion(); + var motion = new AcDream.App.Physics.RemoteMotion(); motion.Body.Position = Vector3.Zero; motion.Body.Orientation = Quaternion.Identity; motion.Body.Velocity = new Vector3(4f, 0f, 5f); @@ -354,7 +354,7 @@ public sealed class RemotePhysicsUpdaterTests new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); - var motion = new GameWindow.RemoteMotion(); + var motion = new AcDream.App.Physics.RemoteMotion(); motion.Body.Orientation = Quaternion.Identity; var entity = new WorldEntity { @@ -384,7 +384,7 @@ public sealed class RemotePhysicsUpdaterTests new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); - var motion = new GameWindow.RemoteMotion(); + var motion = new AcDream.App.Physics.RemoteMotion(); motion.Body.Orientation = Quaternion.Identity; var entity = new WorldEntity { @@ -424,7 +424,7 @@ public sealed class RemotePhysicsUpdaterTests engine, (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); - var motion = new GameWindow.RemoteMotion(); + var motion = new AcDream.App.Physics.RemoteMotion(); motion.Body.Position = new Vector3(191f, 10f, 50f); motion.Body.Orientation = Quaternion.Identity; motion.Body.TransientState = TransientStateFlags.Contact @@ -471,7 +471,7 @@ public sealed class RemotePhysicsUpdaterTests engine, (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); - var motion = new GameWindow.RemoteMotion + var motion = new AcDream.App.Physics.RemoteMotion { Airborne = true, }; @@ -591,7 +591,7 @@ public sealed class RemotePhysicsUpdaterTests using var fixture = new BoundaryRemoteFixture(); LiveEntityRecord oldRecord = fixture.Record; WorldEntity oldEntity = fixture.Entity; - GameWindow.RemoteMotion oldRemote = fixture.Remote; + AcDream.App.Physics.RemoteMotion oldRemote = fixture.Remote; ulong oldAuthority = oldRecord.PositionAuthorityVersion; int publications = 0; @@ -628,9 +628,9 @@ public sealed class RemotePhysicsUpdaterTests using var fixture = new BoundaryRemoteFixture(); LiveEntityRecord oldRecord = fixture.Record; WorldEntity oldEntity = fixture.Entity; - GameWindow.RemoteMotion oldRemote = fixture.Remote; + AcDream.App.Physics.RemoteMotion oldRemote = fixture.Remote; WorldEntity? replacementEntity = null; - GameWindow.RemoteMotion? replacementRemote = null; + AcDream.App.Physics.RemoteMotion? replacementRemote = null; ulong clockEpoch = oldRecord.ObjectClockEpoch; fixture.Live.ProjectionVisibilityChanged += (record, visible) => @@ -813,7 +813,7 @@ public sealed class RemotePhysicsUpdaterTests MeshRefs = Array.Empty(), ParentCellId = cellId, })!; - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); remote.Body.Position = entity.Position; remote.Body.Orientation = entity.Rotation; remote.CellId = cellId; @@ -872,7 +872,7 @@ public sealed class RemotePhysicsUpdaterTests public RemotePhysicsUpdater Updater { get; } public LiveEntityRecord Record { get; } public WorldEntity Entity { get; } - public GameWindow.RemoteMotion Remote { get; } + public AcDream.App.Physics.RemoteMotion Remote { get; } public void HydrateDestination() => Spatial.AddLandblock(new LoadedLandblock( @@ -881,7 +881,7 @@ public sealed class RemotePhysicsUpdaterTests Array.Empty())); public (LiveEntityRecord Record, WorldEntity Entity, - GameWindow.RemoteMotion Remote) ReplaceAtSource() + AcDream.App.Physics.RemoteMotion Remote) ReplaceAtSource() { Assert.True(Live.UnregisterLiveEntity( new DeleteObject.Parsed(Guid, InstanceSequence: 1), @@ -902,7 +902,7 @@ public sealed class RemotePhysicsUpdaterTests } private (LiveEntityRecord Record, WorldEntity Entity, - GameWindow.RemoteMotion Remote) SpawnAndBind( + AcDream.App.Physics.RemoteMotion Remote) SpawnAndBind( ushort instanceSequence, Vector3 position, bool enqueueDestination) @@ -925,7 +925,7 @@ public sealed class RemotePhysicsUpdaterTests MeshRefs = Array.Empty(), ParentCellId = SourceCell, })); - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); remote.Body.Position = position; remote.Body.Orientation = Quaternion.Identity; remote.CellId = SourceCell; @@ -945,7 +945,7 @@ public sealed class RemotePhysicsUpdaterTests private void RegisterShadow( WorldEntity entity, - GameWindow.RemoteMotion remote) + AcDream.App.Physics.RemoteMotion remote) { Engine.ShadowObjects.Register( entity.Id, diff --git a/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs b/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs index 7a261071..441b7780 100644 --- a/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs +++ b/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs @@ -101,7 +101,7 @@ public sealed class RemoteTeleportControllerTests Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); remote.Body.SnapToCell( sourceCell, entity.Position, @@ -242,7 +242,7 @@ public sealed class RemoteTeleportControllerTests Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position); live.SetRemoteMotionRuntime(guid, remote); var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; @@ -333,7 +333,7 @@ public sealed class RemoteTeleportControllerTests Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); remote.Body.SnapToCell(sourceCell, sourcePosition, sourcePosition); remote.Body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable; @@ -774,13 +774,13 @@ public sealed class RemoteTeleportControllerTests Assert.Throws(() => fixture.Live.SetRemoteMotionRuntime( RollbackFixture.Guid, - new GameWindow.RemoteMotion())); + new AcDream.App.Physics.RemoteMotion())); Assert.Throws(() => fixture.Live.SetRemoteMotionRuntime( RollbackFixture.Guid, new BodyOnlyRemote(fixture.Remote.Body))); - var replacement = new GameWindow.RemoteMotion(fixture.Remote.Body); + var replacement = new AcDream.App.Physics.RemoteMotion(fixture.Remote.Body); fixture.Live.SetRemoteMotionRuntime(RollbackFixture.Guid, replacement); fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock)); @@ -811,7 +811,7 @@ public sealed class RemoteTeleportControllerTests Assert.Throws(() => fixture.Live.SetRemoteMotionRuntime( RollbackFixture.Guid, - new GameWindow.RemoteMotion())); + new AcDream.App.Physics.RemoteMotion())); Assert.Throws(() => fixture.Live.SetRemoteMotionRuntime( RollbackFixture.Guid, @@ -1114,7 +1114,7 @@ public sealed class RemoteTeleportControllerTests Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position); live.SetRemoteMotionRuntime(guid, remote); controller = new RemoteTeleportController( @@ -1191,7 +1191,7 @@ public sealed class RemoteTeleportControllerTests Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; - var remote = new GameWindow.RemoteMotion(); + var remote = new AcDream.App.Physics.RemoteMotion(); remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position); live.SetRemoteMotionRuntime(guid, remote); return new LoadedRemoteFixture( @@ -1236,7 +1236,7 @@ public sealed class RemoteTeleportControllerTests private readonly record struct LoadedRemoteFixture( LiveEntityRuntime Live, WorldEntity Entity, - GameWindow.RemoteMotion Remote, + AcDream.App.Physics.RemoteMotion Remote, PhysicsEngine Engine); private static void AddDestination(PhysicsEngine engine) => @@ -1395,7 +1395,7 @@ public sealed class RemoteTeleportControllerTests Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; - Remote = new GameWindow.RemoteMotion(); + Remote = new AcDream.App.Physics.RemoteMotion(); Remote.Body.SnapToCell( celllessSource ? 0u : SourceCell, SourcePosition, @@ -1476,7 +1476,7 @@ public sealed class RemoteTeleportControllerTests internal GpuWorldState Spatial { get; } = new(); internal LiveEntityRuntime Live { get; } internal WorldEntity Entity { get; } - internal GameWindow.RemoteMotion Remote { get; } + internal AcDream.App.Physics.RemoteMotion Remote { get; } internal PhysicsEngine Engine { get; } = new() { DataCache = new PhysicsDataCache() }; internal RemoteTeleportController Controller { get; } internal LiveEntityPresentationController Presentation { get; } diff --git a/tests/AcDream.App.Tests/Physics/RemoteTeleportPlacementTests.cs b/tests/AcDream.App.Tests/Physics/RemoteTeleportPlacementTests.cs index a754d92d..6caac319 100644 --- a/tests/AcDream.App.Tests/Physics/RemoteTeleportPlacementTests.cs +++ b/tests/AcDream.App.Tests/Physics/RemoteTeleportPlacementTests.cs @@ -19,7 +19,7 @@ public sealed class RemoteTeleportPlacementTests 0x01010001u, new Vector3(3f, 4f, 5f), new Vector3(3f, 4f, 5f)); - var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion(body); + var remote = new AcDream.App.Physics.RemoteMotion(body); Quaternion orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f); RemoteTeleportPlacement.Apply( @@ -54,7 +54,7 @@ public sealed class RemoteTeleportPlacementTests State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions, }; var contactPlane = new Plane(Vector3.UnitZ, 0f); - var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion(body) + var remote = new AcDream.App.Physics.RemoteMotion(body) { Airborne = true, }; @@ -97,7 +97,7 @@ public sealed class RemoteTeleportPlacementTests // landblock is absent. SetPositionInternal must still receive the // grounded source edge captured before parking. body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable); - var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion(body) + var remote = new AcDream.App.Physics.RemoteMotion(body) { Airborne = true, }; @@ -141,7 +141,7 @@ public sealed class RemoteTeleportPlacementTests // calc_acceleration, which clears grounded angular drift, before // set_on_walkable installs the steep destination's false value. body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable); - var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion(body); + var remote = new AcDream.App.Physics.RemoteMotion(body); RemoteTeleportPlacement.Apply( remote, @@ -171,7 +171,7 @@ public sealed class RemoteTeleportPlacementTests { var original = new Vector3(7f, 8f, 9f); var body = new PhysicsBody { Position = original }; - var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion(body); + var remote = new AcDream.App.Physics.RemoteMotion(body); Assert.Throws(() => RemoteTeleportPlacement.Apply( diff --git a/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationSchedulerTests.cs b/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationSchedulerTests.cs index ca14c642..c81d5a2b 100644 --- a/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationSchedulerTests.cs +++ b/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationSchedulerTests.cs @@ -52,7 +52,7 @@ public sealed class LiveEntityAnimationSchedulerTests public void HiddenRemotePose_IsSampledOnceAfterAdmittedHiddenQuantum() { var (live, record, animation) = BuildHiddenAnimated(RemoteGuid); - var remote = new GameWindow.RemoteMotion + var remote = new AcDream.App.Physics.RemoteMotion { CellId = Cell, }; @@ -169,7 +169,7 @@ public sealed class LiveEntityAnimationSchedulerTests var (live, record, entity) = BuildMaterialized( RemoteGuid, PhysicsStateFlags.Gravity); - var remote = new GameWindow.RemoteMotion + var remote = new AcDream.App.Physics.RemoteMotion { CellId = Cell, Airborne = true, @@ -286,7 +286,7 @@ public sealed class LiveEntityAnimationSchedulerTests var physics = new PhysicsEngine(); var projectiles = new ProjectileController(live, physics); record.FinalPhysicsState = ProjectileState; - var remote = new GameWindow.RemoteMotion + var remote = new AcDream.App.Physics.RemoteMotion { CellId = Cell, Airborne = false, @@ -598,9 +598,9 @@ public sealed class LiveEntityAnimationSchedulerTests Assert.Equal(1, rootPublishes); } - private static GameWindow.RemoteMotion BuildRemote(WorldEntity entity) + private static AcDream.App.Physics.RemoteMotion BuildRemote(WorldEntity entity) { - var remote = new GameWindow.RemoteMotion + var remote = new AcDream.App.Physics.RemoteMotion { CellId = Cell, }; diff --git a/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs b/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs index d4b59bff..cfbfb27e 100644 --- a/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs +++ b/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs @@ -30,7 +30,7 @@ namespace AcDream.Core.Tests.Physics.Motion; // // This harness wires a real MotionInterpreter + AnimationSequencer + // MotionTableDispatchSink + MoveToManager EXACTLY like GameWindow's -// EnsureRemoteMotionBindings (GameWindow.cs:4251) and ticks them in +// EnsureRemoteMotionBindings and ticks them in // the retail per-object order: CSequence update → PositionManager adjust → // complete Frame combine → hook processing → Target/Movement/PartArray/ // Position manager tail. Wire events (aggro stance UM, mt-6 arms, attack @@ -81,8 +81,8 @@ internal sealed class RemoteChaseHarness public const float OwnRadius = 0.3f; public const float StickyTargetRadius = 0.5f; - // Field-for-field mirror of GameWindow's RemoteMotion construction - // (GameWindow.cs:592-618): Contact+OnWalkable+Active, InWorld=true (the + // Field-for-field mirror of AcDream.App.Physics.RemoteMotion construction: + // Contact+OnWalkable+Active, InWorld=true (the // R4-V5 door fix — without it the interp's detached-object guard strips // every dispatched transition link), and the R4-V5 #160 RemoteWeenie // (null weenie degrades run-rate to 1.0). @@ -98,7 +98,7 @@ internal sealed class RemoteChaseHarness public readonly AnimationSequencer Seq; public readonly MotionTableDispatchSink Sink; - /// R5-V5: GameWindow's RemoteMotion.Movement twin — the ONE + /// R5-V5: AcDream.App.Physics.RemoteMotion.Movement twin — the ONE /// per-entity MovementManager facade owning Interp + the MoveToManager /// (retail CPhysicsObj::movement_manager). public readonly MovementManager Movement; @@ -156,7 +156,7 @@ internal sealed class RemoteChaseHarness WeenieObj = new RemoteWeenie(), }; - // ── EnsureRemoteMotionBindings (GameWindow.cs:4251) verbatim ── + // ── EnsureRemoteMotionBindings verbatim ── Sink = new MotionTableDispatchSink(Seq); Interp.DefaultSink = Sink; // #174: production binds the seam to Manager.HandleEnterWorld @@ -167,7 +167,7 @@ internal sealed class RemoteChaseHarness Interp.CheckForCompletedMotions = Seq.Manager.CheckForCompletedMotions; // ── R5-V5: the MovementManager facade owns Interp + the moveto — - // GameWindow's RemoteMotion ctor + EnsureRemoteMotionBindings + // AcDream.App.Physics.RemoteMotion ctor + EnsureRemoteMotionBindings // factory shape verbatim (sticky binds inside the factory; // MakeMoveToManager after the host/Pm exist). ── Movement = new MovementManager(Interp)