refactor(physics): extract remote motion runtime

This commit is contained in:
Erik 2026-07-21 13:17:58 +02:00
parent d68c83d1a5
commit 5882b308c1
12 changed files with 368 additions and 361 deletions

View file

@ -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;

View file

@ -0,0 +1,307 @@
using AcDream.App.Rendering;
using AcDream.App.World;
namespace AcDream.App.Physics;
/// <summary>
/// Per-remote-entity physics + motion stack — verbatim application of
/// retail's client-side motion pipeline to every remote. Mirrors retail
/// <c>CPhysicsObj::update_object</c> (0x00515D10) →
/// <c>CPhysicsObj::UpdateObjectInternal</c> (0x005156B0) →
/// <c>CPhysicsObj::UpdatePositionInternal</c> (0x00512C30) →
/// <c>CPhysicsObj::UpdatePhysicsInternal</c> (0x00510700), and ACE's
/// <c>PhysicsObj.cs</c> port.
///
/// <para>
/// 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 <see cref="AcDream.Core.Physics.PhysicsBody"/> with its
/// <see cref="AcDream.Core.Physics.MotionInterpreter"/> so each
/// remote gets the same treatment as the local player.
/// </para>
/// </summary>
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;
/// <summary>R5-V5: retail <c>CPhysicsObj::movement_manager</c> — the
/// ONE per-entity owner of the interp + moveto pair (acclient.h
/// <c>/* 3463 */</c>). Constructed here with the interp; the
/// MoveToManager side arrives via <c>MoveToFactory</c> +
/// <c>MakeMoveToManager()</c> in EnsureRemoteMotionBindings.
/// <see cref="Motion"/>/<see cref="MoveTo"/> below are views of its
/// children (retail <c>get_minterp</c>-style access), kept so the
/// dozens of existing call sites read unchanged.</summary>
public AcDream.Core.Physics.Motion.MovementManager Movement;
public AcDream.Core.Physics.MotionInterpreter Motion => Movement.Minterp;
/// <summary>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).</summary>
public AcDream.Core.Physics.Motion.MotionTableDispatchSink? Sink;
/// <summary>Last UpdatePosition timestamp — drives body.update_object sub-stepping.</summary>
public double LastServerPosTime;
/// <summary>Last known server position — kept for diagnostics / HUD.</summary>
public System.Numerics.Vector3 LastServerPos;
/// <summary>
/// #184: the body position at which this remote's collision SHADOW was
/// last (re-)registered. The per-tick shadow-follows-resolved sync
/// (<c>SyncRemoteShadowToBody</c>) skips re-flooding when the body has
/// not moved &gt; ε since this — a resting/idle-town crowd doesn't churn
/// the registry, while a moving/de-overlapping crowd re-registers every
/// tick. Sentinel <c>Zero</c> forces the first sync.
/// </summary>
public System.Numerics.Vector3 LastShadowSyncPos;
/// <summary>
/// Complete root orientation paired with <see cref="LastShadowSyncPos"/>.
/// 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.
/// </summary>
public System.Numerics.Quaternion LastShadowSyncOrientation;
/// <summary>
/// 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.
/// </summary>
public System.Numerics.Vector3 ServerVelocity;
public bool HasServerVelocity;
/// <summary>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 <see cref="Movement"/>; this is the child view.</summary>
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;
/// <summary>
/// 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
/// <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> instead of
/// the InterpretedMotionState path.
/// </summary>
public bool ServerMoveToActive;
/// <summary>
/// True once a MoveTo packet's full path payload (Origin + thresholds)
/// has been parsed and the world-converted destination is stored on
/// <see cref="MoveToDestinationWorld"/>. Cleared on arrival or when
/// the next non-MoveTo UpdateMotion replaces the locomotion source.
/// Phase L.1c (2026-04-28).
/// </summary>
public bool HasMoveToDestination;
/// <summary>
/// World-space destination from the most recent MoveTo packet's
/// <c>Origin</c> field, converted via the same landblock-grid
/// arithmetic <c>OnLivePositionUpdated</c> uses.
/// </summary>
public System.Numerics.Vector3 MoveToDestinationWorld;
/// <summary>
/// <c>min_distance</c> from the MoveTo packet's MovementParameters.
/// Used by <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> as
/// the chase-arrival threshold per retail
/// <c>MoveToManager::HandleMoveToPosition</c>.
/// </summary>
public float MoveToMinDistance;
/// <summary>
/// <c>distance_to_object</c> from the MoveTo packet. Reserved for
/// the flee branch (<c>move_away</c>); chase uses
/// <see cref="MoveToMinDistance"/>.
/// </summary>
public float MoveToDistanceToObject;
/// <summary>
/// True if MovementParameters bit 9 (<c>move_towards</c>, mask
/// <c>0x200</c>) is set on the active packet — i.e. this is a
/// chase. False = flee (<c>move_away</c>) or static target.
/// </summary>
public bool MoveToMoveTowards;
/// <summary>
/// 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
/// <see cref="AcDream.Core.Physics.RemoteMoveToDriver.StaleDestinationSeconds"/>
/// — typically because the entity left our streaming view and
/// the server stopped broadcasting its MoveTo updates.
/// </summary>
public double LastMoveToPacketTime;
/// <summary>
/// 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
/// <see cref="AcDream.Core.Physics.PhysicsEngine.ResolveWithTransition"/>
/// 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).
/// </summary>
private uint _cellId;
private Func<uint>? _canonicalCellReader;
private Action<uint>? _canonicalCellWriter;
/// <summary>
/// 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
/// <see cref="LiveEntityRuntime"/>, so projectile prediction,
/// Movement packets, and spatial rebucketing cannot develop competing
/// cell identities.
/// </summary>
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();
/// <summary>
/// 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.
/// </summary>
public bool Airborne;
/// <summary>
/// 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 <see cref="Position"/>.
/// </summary>
public AcDream.Core.Physics.InterpolationManager Interp { get; } =
new AcDream.Core.Physics.InterpolationManager();
/// <summary>
/// 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.
/// </summary>
public AcDream.Core.Physics.RemoteMotionCombiner Position { get; } =
new AcDream.Core.Physics.RemoteMotionCombiner();
/// <summary>Reusable complete delta frame shared by interpolation,
/// Sticky/Constraint, and the final body composition.</summary>
public AcDream.Core.Physics.Motion.MotionDeltaFrame PositionManagerDeltaScratch { get; } =
new AcDream.Core.Physics.Motion.MotionDeltaFrame();
/// <summary>
/// Diagnostic-only (gated on <c>ACDREAM_REMOTE_VEL_DIAG=1</c>): the
/// previous UpdatePosition's world position + timestamp. The per-tick
/// path compares the server's broadcast pace with the literal root
/// displacement emitted by <c>CSequence::update</c>. The old inferred
/// walk/run velocity was the source of the periodic overshoot this
/// diagnostic was introduced to find.
/// </summary>
public System.Numerics.Vector3 PrevServerPos;
public double PrevServerPosTime;
public double LastOmegaDiagLogTime;
/// <summary>
/// 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.
/// </summary>
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<uint> read, Action<uint> write)
{
_canonicalCellReader = read ?? throw new ArgumentNullException(nameof(read));
_canonicalCellWriter = write ?? throw new ArgumentNullException(nameof(write));
}
}

View file

@ -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;

View file

@ -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;
/// <summary>
/// Per-remote-entity physics + motion stack — verbatim application of
/// retail's client-side motion pipeline to every remote. Mirrors
/// retail <c>FUN_00515020</c> <c>update_object</c> → <c>FUN_00513730</c>
/// <c>UpdatePositionInternal</c> → <c>FUN_005111D0</c>
/// <c>UpdatePhysicsInternal</c>, and ACE's <c>PhysicsObj.cs</c> port.
///
/// <para>
/// 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 <see cref="AcDream.Core.Physics.PhysicsBody"/> with its
/// <see cref="AcDream.Core.Physics.MotionInterpreter"/> so each
/// remote gets the same treatment as the local player.
/// </para>
/// </summary>
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;
/// <summary>R5-V5: retail <c>CPhysicsObj::movement_manager</c> — the
/// ONE per-entity owner of the interp + moveto pair (acclient.h
/// <c>/* 3463 */</c>). Constructed here with the interp; the
/// MoveToManager side arrives via <c>MoveToFactory</c> +
/// <c>MakeMoveToManager()</c> in EnsureRemoteMotionBindings.
/// <see cref="Motion"/>/<see cref="MoveTo"/> below are views of its
/// children (retail <c>get_minterp</c>-style access), kept so the
/// dozens of existing call sites read unchanged.</summary>
public AcDream.Core.Physics.Motion.MovementManager Movement;
public AcDream.Core.Physics.MotionInterpreter Motion => Movement.Minterp;
/// <summary>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).</summary>
public AcDream.Core.Physics.Motion.MotionTableDispatchSink? Sink;
/// <summary>Last UpdatePosition timestamp — drives body.update_object sub-stepping.</summary>
public double LastServerPosTime;
/// <summary>Last known server position — kept for diagnostics / HUD.</summary>
public System.Numerics.Vector3 LastServerPos;
/// <summary>
/// #184: the body position at which this remote's collision SHADOW was
/// last (re-)registered. The per-tick shadow-follows-resolved sync
/// (<c>SyncRemoteShadowToBody</c>) skips re-flooding when the body has
/// not moved &gt; ε since this — a resting/idle-town crowd doesn't churn
/// the registry, while a moving/de-overlapping crowd re-registers every
/// tick. Sentinel <c>Zero</c> forces the first sync.
/// </summary>
public System.Numerics.Vector3 LastShadowSyncPos;
/// <summary>
/// Complete root orientation paired with <see cref="LastShadowSyncPos"/>.
/// 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.
/// </summary>
public System.Numerics.Quaternion LastShadowSyncOrientation;
/// <summary>
/// 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.
/// </summary>
public System.Numerics.Vector3 ServerVelocity;
public bool HasServerVelocity;
/// <summary>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 <see cref="Movement"/>; this is the child view.</summary>
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;
/// <summary>
/// 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
/// <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> instead of
/// the InterpretedMotionState path.
/// </summary>
public bool ServerMoveToActive;
/// <summary>
/// True once a MoveTo packet's full path payload (Origin + thresholds)
/// has been parsed and the world-converted destination is stored on
/// <see cref="MoveToDestinationWorld"/>. Cleared on arrival or when
/// the next non-MoveTo UpdateMotion replaces the locomotion source.
/// Phase L.1c (2026-04-28).
/// </summary>
public bool HasMoveToDestination;
/// <summary>
/// World-space destination from the most recent MoveTo packet's
/// <c>Origin</c> field, converted via the same landblock-grid
/// arithmetic <c>OnLivePositionUpdated</c> uses.
/// </summary>
public System.Numerics.Vector3 MoveToDestinationWorld;
/// <summary>
/// <c>min_distance</c> from the MoveTo packet's MovementParameters.
/// Used by <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> as
/// the chase-arrival threshold per retail
/// <c>MoveToManager::HandleMoveToPosition</c>.
/// </summary>
public float MoveToMinDistance;
/// <summary>
/// <c>distance_to_object</c> from the MoveTo packet. Reserved for
/// the flee branch (<c>move_away</c>); chase uses
/// <see cref="MoveToMinDistance"/>.
/// </summary>
public float MoveToDistanceToObject;
/// <summary>
/// True if MovementParameters bit 9 (<c>move_towards</c>, mask
/// <c>0x200</c>) is set on the active packet — i.e. this is a
/// chase. False = flee (<c>move_away</c>) or static target.
/// </summary>
public bool MoveToMoveTowards;
/// <summary>
/// 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
/// <see cref="AcDream.Core.Physics.RemoteMoveToDriver.StaleDestinationSeconds"/>
/// — typically because the entity left our streaming view and
/// the server stopped broadcasting its MoveTo updates.
/// </summary>
public double LastMoveToPacketTime;
/// <summary>
/// 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
/// <see cref="AcDream.Core.Physics.PhysicsEngine.ResolveWithTransition"/>
/// 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).
/// </summary>
private uint _cellId;
private Func<uint>? _canonicalCellReader;
private Action<uint>? _canonicalCellWriter;
/// <summary>
/// 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
/// <see cref="LiveEntityRuntime"/>, so projectile prediction,
/// Movement packets, and spatial rebucketing cannot develop competing
/// cell identities.
/// </summary>
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();
/// <summary>
/// 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.
/// </summary>
public bool Airborne;
/// <summary>
/// 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 <see cref="Position"/>.
/// </summary>
public AcDream.Core.Physics.InterpolationManager Interp { get; } =
new AcDream.Core.Physics.InterpolationManager();
/// <summary>
/// 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.
/// </summary>
public AcDream.Core.Physics.RemoteMotionCombiner Position { get; } =
new AcDream.Core.Physics.RemoteMotionCombiner();
/// <summary>Reusable complete delta frame shared by interpolation,
/// Sticky/Constraint, and the final body composition.</summary>
public AcDream.Core.Physics.Motion.MotionDeltaFrame PositionManagerDeltaScratch { get; } =
new AcDream.Core.Physics.Motion.MotionDeltaFrame();
/// <summary>
/// Diagnostic-only (gated on <c>ACDREAM_REMOTE_VEL_DIAG=1</c>): the
/// previous UpdatePosition's world position + timestamp. The per-tick
/// path compares the server's broadcast pace with the literal root
/// displacement emitted by <c>CSequence::update</c>. The old inferred
/// walk/run velocity was the source of the periodic overshoot this
/// diagnostic was introduced to find.
/// </summary>
public System.Numerics.Vector3 PrevServerPos;
public double PrevServerPosTime;
public double LastOmegaDiagLogTime;
/// <summary>
/// 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.
/// </summary>
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<uint> read, Action<uint> write)
{
_canonicalCellReader = read ?? throw new ArgumentNullException(nameof(read));
_canonicalCellWriter = write ?? throw new ArgumentNullException(nameof(write));
}
}
/// <summary>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.</summary>

View file

@ -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;

View file

@ -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<MeshRef>(),
ParentCellId = SourceCell,
})!;
var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion
var remote = new AcDream.App.Physics.RemoteMotion
{
CellId = SourceCell,
};

View file

@ -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);

View file

@ -68,7 +68,7 @@ public sealed class RemotePhysicsUpdaterTests
MeshRefs = Array.Empty<MeshRef>(),
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<LiveAnimationPartTemplate>(),
PartAvailability = Array.Empty<bool>(),
};
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<LiveAnimationPartTemplate>(),
PartAvailability = Array.Empty<bool>(),
};
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<LiveAnimationPartTemplate>(),
PartAvailability = Array.Empty<bool>(),
};
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<MeshRef>(),
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<WorldEntity>()));
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<MeshRef>(),
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,

View file

@ -101,7 +101,7 @@ public sealed class RemoteTeleportControllerTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
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<MeshRef>(),
})!;
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<MeshRef>(),
})!;
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<InvalidOperationException>(() =>
fixture.Live.SetRemoteMotionRuntime(
RollbackFixture.Guid,
new GameWindow.RemoteMotion()));
new AcDream.App.Physics.RemoteMotion()));
Assert.Throws<InvalidOperationException>(() =>
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<InvalidOperationException>(() =>
fixture.Live.SetRemoteMotionRuntime(
RollbackFixture.Guid,
new GameWindow.RemoteMotion()));
new AcDream.App.Physics.RemoteMotion()));
Assert.Throws<InvalidOperationException>(() =>
fixture.Live.SetRemoteMotionRuntime(
RollbackFixture.Guid,
@ -1114,7 +1114,7 @@ public sealed class RemoteTeleportControllerTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
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<MeshRef>(),
})!;
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<MeshRef>(),
})!;
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; }

View file

@ -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<ArgumentOutOfRangeException>(() =>
RemoteTeleportPlacement.Apply(

View file

@ -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,
};

View file

@ -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;
/// <summary>R5-V5: GameWindow's RemoteMotion.Movement twin — the ONE
/// <summary>R5-V5: AcDream.App.Physics.RemoteMotion.Movement twin — the ONE
/// per-entity MovementManager facade owning Interp + the MoveToManager
/// (retail CPhysicsObj::movement_manager).</summary>
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)